409 lines
11 KiB
Go
409 lines
11 KiB
Go
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)
|
||
}
|