Initial UNC file watcher web console
This commit is contained in:
+408
@@ -0,0 +1,408 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user