Files

409 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}