package main import ( "encoding/json" "errors" "fmt" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) func defaultConfig() config { return config{ interval: defaultInterval, stableFor: defaultStableFor, stableScans: 2, mode: "copy", generic: genericRule{ dedupeMode: genericDedupeNameSize, }, } } func resolveSettingsPath() string { executablePath, err := os.Executable() if err == nil { return filepath.Join(filepath.Dir(executablePath), settingsFileName) } return settingsFileName } func loadSettingsErrorMessage(err error) string { if err == nil { return "" } return fmt.Sprintf("读取本地配置失败: %v", err) } func loadSettings(path string) (formValues, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return formValues{}, nil } return formValues{}, err } var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return formValues{}, err } if hasAnyLegacyKeys(raw) { var legacy legacyFormValues if err := json.Unmarshal(data, &legacy); err != nil { return formValues{}, err } return formValues{ ArchiveEnabled: legacy.SourceDir != "" || legacy.ZipDir != "" || legacy.UnzipDir != "", ArchiveSourceDir: legacy.SourceDir, ArchiveZipDir: legacy.ZipDir, ArchiveUnzipDir: legacy.UnzipDir, GenericDedupe: genericDedupeNameSize, Interval: legacy.Interval, StableFor: legacy.StableFor, StableScans: legacy.StableScans, Mode: legacy.Mode, Recursive: legacy.Recursive, }, nil } var form formValues if err := json.Unmarshal(data, &form); err != nil { return formValues{}, err } if _, ok := raw["archive_enabled"]; !ok && hasArchiveFields(form) { form.ArchiveEnabled = true } if _, ok := raw["generic_enabled"]; !ok && hasGenericFields(form) { form.GenericEnabled = true } if form.GenericDedupe == "" { form.GenericDedupe = genericDedupeNameSize } return form, nil } func hasAnyLegacyKeys(raw map[string]json.RawMessage) bool { _, sourceOK := raw["source_dir"] _, zipOK := raw["zip_dir"] _, unzipOK := raw["unzip_dir"] return sourceOK || zipOK || unzipOK } func saveSettings(path string, form formValues) error { data, err := json.MarshalIndent(form, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0o644) } func mergeForm(base, saved formValues) formValues { if saved.ArchiveSourceDir != "" { base.ArchiveSourceDir = saved.ArchiveSourceDir } if saved.ArchiveZipDir != "" { base.ArchiveZipDir = saved.ArchiveZipDir } if saved.ArchiveUnzipDir != "" { base.ArchiveUnzipDir = saved.ArchiveUnzipDir } if saved.GenericSourceDir != "" { base.GenericSourceDir = saved.GenericSourceDir } if saved.GenericTargetDir != "" { base.GenericTargetDir = saved.GenericTargetDir } if saved.GenericExts != "" { base.GenericExts = saved.GenericExts } if saved.GenericDedupe != "" { base.GenericDedupe = saved.GenericDedupe } if saved.Interval != "" { base.Interval = saved.Interval } if saved.StableFor != "" { base.StableFor = saved.StableFor } if saved.StableScans != "" { base.StableScans = saved.StableScans } if saved.Mode != "" { base.Mode = saved.Mode } base.ArchiveEnabled = saved.ArchiveEnabled base.GenericEnabled = saved.GenericEnabled base.Recursive = saved.Recursive return base } func formFromRequest(r *http.Request) formValues { _ = r.ParseForm() return formValues{ ArchiveEnabled: r.FormValue("archive_enabled") != "", ArchiveSourceDir: strings.TrimSpace(r.FormValue("archive_source")), ArchiveZipDir: strings.TrimSpace(r.FormValue("archive_zip")), ArchiveUnzipDir: strings.TrimSpace(r.FormValue("archive_unzip")), GenericEnabled: r.FormValue("generic_enabled") != "", GenericSourceDir: strings.TrimSpace(r.FormValue("generic_source")), GenericTargetDir: strings.TrimSpace(r.FormValue("generic_target")), GenericExts: strings.TrimSpace(r.FormValue("generic_exts")), GenericDedupe: strings.TrimSpace(r.FormValue("generic_dedupe")), Interval: strings.TrimSpace(r.FormValue("interval")), StableFor: strings.TrimSpace(r.FormValue("stable_for")), StableScans: strings.TrimSpace(r.FormValue("stable_scans")), Mode: strings.TrimSpace(r.FormValue("mode")), Recursive: r.FormValue("recursive") != "", } } func formFromConfig(cfg config) formValues { return formValues{ ArchiveEnabled: cfg.archive.enabled, ArchiveSourceDir: cfg.archive.sourceDir, ArchiveZipDir: cfg.archive.zipDir, ArchiveUnzipDir: cfg.archive.unzipDir, GenericEnabled: cfg.generic.enabled, GenericSourceDir: cfg.generic.sourceDir, GenericTargetDir: cfg.generic.targetDir, GenericExts: strings.Join(cfg.generic.extensions, ","), GenericDedupe: cfg.generic.dedupeMode, Interval: cfg.interval.String(), StableFor: cfg.stableFor.String(), StableScans: strconv.Itoa(cfg.stableScans), Mode: cfg.mode, Recursive: cfg.recursive, } } func hasFormInput(form formValues) bool { return form.ArchiveEnabled || form.ArchiveSourceDir != "" || form.ArchiveZipDir != "" || form.ArchiveUnzipDir != "" || form.GenericEnabled || form.GenericSourceDir != "" || form.GenericTargetDir != "" || form.GenericExts != "" || form.GenericDedupe != "" || form.Interval != "" || form.StableFor != "" || form.StableScans != "" || form.Mode != "" || form.Recursive } func hasArchiveFields(form formValues) bool { return form.ArchiveSourceDir != "" || form.ArchiveZipDir != "" || form.ArchiveUnzipDir != "" } func hasGenericFields(form formValues) bool { return form.GenericSourceDir != "" || form.GenericTargetDir != "" || form.GenericExts != "" } func parseConfig(form formValues) (config, error) { cfg := defaultConfig() cfg.archive = archiveRule{ enabled: form.ArchiveEnabled, sourceDir: normalizePath(form.ArchiveSourceDir), zipDir: normalizePath(form.ArchiveZipDir), unzipDir: normalizePath(form.ArchiveUnzipDir), } cfg.generic = genericRule{ enabled: form.GenericEnabled, sourceDir: normalizePath(form.GenericSourceDir), targetDir: normalizePath(form.GenericTargetDir), dedupeMode: strings.ToLower(strings.TrimSpace(form.GenericDedupe)), } cfg.mode = strings.ToLower(strings.TrimSpace(form.Mode)) cfg.recursive = form.Recursive if form.Interval != "" { duration, err := time.ParseDuration(form.Interval) if err != nil { return cfg, fmt.Errorf("扫描间隔格式不正确: %w", err) } cfg.interval = duration } if form.StableFor != "" { duration, err := time.ParseDuration(form.StableFor) if err != nil { return cfg, fmt.Errorf("稳定时长格式不正确: %w", err) } cfg.stableFor = duration } if form.StableScans != "" { stableScans, err := strconv.Atoi(form.StableScans) if err != nil { return cfg, errors.New("稳定扫描次数必须是整数") } cfg.stableScans = stableScans } if form.GenericExts != "" { extensions, err := parseExtensions(form.GenericExts) if err != nil { return cfg, err } cfg.generic.extensions = extensions } return validateConfig(cfg) } func validateConfig(cfg config) (config, error) { if cfg.mode == "" { cfg.mode = "copy" } if cfg.generic.dedupeMode == "" { cfg.generic.dedupeMode = genericDedupeNameSize } switch cfg.mode { case "copy", "move": default: return cfg, fmt.Errorf("不支持的处理方式 %q", cfg.mode) } switch cfg.generic.dedupeMode { case genericDedupeNameSize, genericDedupeNameHash: default: return cfg, fmt.Errorf("不支持的普通文件去重方式 %q", cfg.generic.dedupeMode) } if cfg.interval <= 0 { return cfg, errors.New("扫描间隔必须大于 0") } if cfg.stableFor < 0 { return cfg, errors.New("稳定时长不能小于 0") } if cfg.stableScans < 1 { return cfg, errors.New("稳定扫描次数必须至少为 1") } if !cfg.archive.enabled && !cfg.generic.enabled { return cfg, errors.New("至少启用一条监听规则") } if cfg.archive.enabled { if cfg.archive.sourceDir == "" { return cfg, errors.New("压缩包规则的 UNC 源路径不能为空") } if cfg.archive.zipDir == "" { return cfg, errors.New("压缩包规则的 zip 路径不能为空") } if cfg.archive.unzipDir == "" { return cfg, errors.New("压缩包规则的 unzip 路径不能为空") } if cfg.archive.sourceDir == cfg.archive.zipDir { return cfg, errors.New("压缩包规则的源路径和 zip 路径不能相同") } if cfg.archive.sourceDir == cfg.archive.unzipDir { return cfg, errors.New("压缩包规则的源路径和 unzip 路径不能相同") } if cfg.archive.zipDir == cfg.archive.unzipDir { return cfg, errors.New("压缩包规则的 zip 路径和 unzip 路径不能相同") } } if cfg.generic.enabled { if cfg.generic.sourceDir == "" { return cfg, errors.New("普通文件规则的 UNC 源路径不能为空") } if cfg.generic.targetDir == "" { return cfg, errors.New("普通文件规则的目标路径不能为空") } if cfg.generic.sourceDir == cfg.generic.targetDir { return cfg, errors.New("普通文件规则的源路径和目标路径不能相同") } if len(cfg.generic.extensions) == 0 { return cfg, errors.New("普通文件规则的扩展名列表不能为空") } } return cfg, nil } func parseExtensions(raw string) ([]string, error) { parts := strings.FieldsFunc(raw, func(r rune) bool { switch r { case ',', ';', '\n', '\r', '\t', ' ': return true default: return false } }) seen := make(map[string]struct{}) extensions := make([]string, 0, len(parts)) for _, part := range parts { item := strings.ToLower(strings.TrimSpace(part)) item = strings.TrimPrefix(item, "*") if item == "" { continue } if !strings.HasPrefix(item, ".") { item = "." + item } if item == "." { continue } if _, ok := seen[item]; ok { continue } seen[item] = struct{}{} extensions = append(extensions, item) } if len(extensions) == 0 { return nil, errors.New("普通文件规则的扩展名列表不能为空") } return extensions, nil } func matchesExtensions(name string, extensions []string) bool { lowerName := strings.ToLower(name) for _, ext := range extensions { if strings.HasSuffix(lowerName, ext) { return true } } return false } func normalizePath(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } return filepath.Clean(raw) } func joinMessages(primary, secondary string) string { if primary == "" { return secondary } if secondary == "" { return primary } return primary + ";" + secondary } func saveErrorMessage(err error) string { if err == nil { return "" } return fmt.Sprintf("配置保存失败: %v", err) }