Initial UNC file watcher web console

This commit is contained in:
Codex
2026-04-17 17:08:49 +08:00
commit 1d283d540c
10 changed files with 1931 additions and 0 deletions
+318
View File
@@ -0,0 +1,318 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
)
func newApp() *app {
defaultCfg := defaultConfig()
defaultForm := formFromConfig(defaultCfg)
settingsPath := resolveSettingsPath()
savedForm, err := loadSettings(settingsPath)
if err != nil {
log.Printf("load settings failed: %v", err)
}
return &app{
cfg: defaultCfg,
form: mergeForm(defaultForm, savedForm),
settingsPath: settingsPath,
lastError: loadSettingsErrorMessage(err),
}
}
func (a *app) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
a.renderPage(w, formValues{}, "", "")
}
func (a *app) handleStart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
form := formFromRequest(r)
cfg, err := parseConfig(form)
if err != nil {
a.renderPage(w, form, "", err.Error())
return
}
normalizedForm := formFromConfig(cfg)
saveErr := a.persistForm(normalizedForm)
if err := a.startWatcher(cfg); err != nil {
a.renderPage(w, normalizedForm, "", joinMessages(err.Error(), saveErrorMessage(saveErr)))
return
}
a.renderPage(w, normalizedForm, "监听任务已启动,配置已保存到本地", saveErrorMessage(saveErr))
}
func (a *app) handleStop(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
message := "当前没有正在运行的监听任务"
if a.stopWatcher() {
message = "已发送停止请求"
}
a.renderPage(w, formValues{}, message, "")
}
func (a *app) persistForm(form formValues) error {
a.mu.Lock()
a.form = form
a.mu.Unlock()
if err := saveSettings(a.settingsPath, form); err != nil {
a.addLogf("save settings failed: %v", err)
return err
}
a.mu.Lock()
if strings.HasPrefix(a.lastError, "读取本地配置失败") || strings.HasPrefix(a.lastError, "配置保存失败") {
a.lastError = ""
}
a.mu.Unlock()
return nil
}
func (a *app) startWatcher(cfg config) error {
cfg, err := validateConfig(cfg)
if err != nil {
return err
}
if cfg.archive.enabled {
if err := ensureExistingDir(cfg.archive.sourceDir); err != nil {
return fmt.Errorf("压缩包规则源路径错误: %w", err)
}
if err := ensureDir(cfg.archive.zipDir); err != nil {
return fmt.Errorf("压缩包规则 zip 路径错误: %w", err)
}
if err := ensureDir(cfg.archive.unzipDir); err != nil {
return fmt.Errorf("压缩包规则 unzip 路径错误: %w", err)
}
}
if cfg.generic.enabled {
if err := ensureExistingDir(cfg.generic.sourceDir); err != nil {
return fmt.Errorf("普通文件规则源路径错误: %w", err)
}
if err := ensureDir(cfg.generic.targetDir); err != nil {
return fmt.Errorf("普通文件规则目标路径错误: %w", err)
}
}
ctx, cancel := context.WithCancel(context.Background())
a.mu.Lock()
if a.cancel != nil {
a.cancel()
}
a.runID++
runID := a.runID
a.cancel = cancel
a.running = true
a.startedAt = time.Now()
a.lastError = ""
a.lastMessage = "监听任务运行中"
a.cfg = cfg
a.form = formFromConfig(cfg)
a.mu.Unlock()
if cfg.archive.enabled {
a.addLogf("archive watcher enabled: source=%q zip=%q unzip=%q", cfg.archive.sourceDir, cfg.archive.zipDir, cfg.archive.unzipDir)
}
if cfg.generic.enabled {
a.addLogf(
"generic watcher enabled: source=%q target=%q extensions=%s dedupe=%s",
cfg.generic.sourceDir,
cfg.generic.targetDir,
strings.Join(cfg.generic.extensions, ", "),
cfg.generic.dedupeMode,
)
}
a.addLogf("common settings: interval=%s stable_for=%s stable_scans=%d mode=%s recursive=%t",
cfg.interval, cfg.stableFor, cfg.stableScans, cfg.mode, cfg.recursive)
go a.runWatcher(ctx, runID, cfg)
return nil
}
func (a *app) stopWatcher() bool {
a.mu.RLock()
cancel := a.cancel
running := a.running
a.mu.RUnlock()
if cancel == nil || !running {
return false
}
a.addLogf("stop requested from web UI")
cancel()
return true
}
func (a *app) runWatcher(ctx context.Context, runID int64, cfg config) {
defer a.finishRun(runID)
archiveState := make(map[string]fileState)
genericState := make(map[string]fileState)
runScan := func() {
var errorsFound []string
if cfg.archive.enabled {
err := scanRule(
"压缩包规则",
cfg.archive.sourceDir,
cfg.recursive,
func(name string) bool {
return strings.HasSuffix(strings.ToLower(name), tarGzSuffix)
},
cfg,
archiveState,
func(sourcePath string) (string, bool, error) {
return processArchiveCandidate(cfg, sourcePath)
},
a.addLogf,
)
if err != nil {
errorsFound = append(errorsFound, "压缩包规则扫描失败: "+err.Error())
}
}
if cfg.generic.enabled {
err := scanRule(
"普通文件规则",
cfg.generic.sourceDir,
cfg.recursive,
func(name string) bool {
return matchesExtensions(name, cfg.generic.extensions)
},
cfg,
genericState,
func(sourcePath string) (string, bool, error) {
return processGenericCandidate(cfg, sourcePath)
},
a.addLogf,
)
if err != nil {
errorsFound = append(errorsFound, "普通文件规则扫描失败: "+err.Error())
}
}
if len(errorsFound) > 0 {
message := strings.Join(errorsFound, "")
a.setLastError(message)
a.addLogf("%s", message)
return
}
a.setLastError("")
}
runScan()
ticker := time.NewTicker(cfg.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
a.addLogf("watcher stopped")
return
case <-ticker.C:
runScan()
}
}
}
func (a *app) finishRun(runID int64) {
a.mu.Lock()
defer a.mu.Unlock()
if a.runID != runID {
return
}
a.running = false
a.cancel = nil
a.lastMessage = "监听任务已停止"
}
func (a *app) setLastError(message string) {
a.mu.Lock()
defer a.mu.Unlock()
a.lastError = message
}
func (a *app) addLogf(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
log.Print(msg)
line := fmt.Sprintf("%s %s", time.Now().Format("2006-01-02 15:04:05"), msg)
a.mu.Lock()
defer a.mu.Unlock()
a.logs = append(a.logs, line)
if len(a.logs) > maxLogLines {
a.logs = a.logs[len(a.logs)-maxLogLines:]
}
}
func (a *app) renderPage(w http.ResponseWriter, override formValues, submitMessage, submitError string) {
page := a.snapshot()
if hasFormInput(override) {
page.Form = override
}
page.SubmitMessage = submitMessage
page.SubmitError = submitError
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := pageTemplate.Execute(w, page); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (a *app) snapshot() pageData {
a.mu.RLock()
defer a.mu.RUnlock()
logs := append([]string(nil), a.logs...)
startedAt := ""
if !a.startedAt.IsZero() {
startedAt = a.startedAt.Format("2006-01-02 15:04:05")
}
return pageData{
Running: a.running,
StartedAt: startedAt,
LastMessage: a.lastMessage,
LastError: a.lastError,
SettingsPath: a.settingsPath,
Form: a.form,
Logs: logs,
}
}