362 lines
11 KiB
Go
362 lines
11 KiB
Go
package main
|
|
|
|
import "html/template"
|
|
|
|
var pageTemplate = template.Must(template.New("page").Parse(`<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>UNC 文件监听控制台</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f3efe7;
|
|
--panel: #fffaf2;
|
|
--panel-strong: #f7efe1;
|
|
--ink: #1f2520;
|
|
--muted: #6b716a;
|
|
--accent: #205c43;
|
|
--accent-soft: #dcebdd;
|
|
--warn: #8a3b2f;
|
|
--warn-soft: #f7ddd8;
|
|
--line: #d8cfbf;
|
|
--shadow: rgba(31, 37, 32, 0.08);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
background:
|
|
radial-gradient(circle at top left, rgba(32, 92, 67, 0.10), transparent 28%),
|
|
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
|
|
color: var(--ink);
|
|
}
|
|
.shell {
|
|
width: min(1180px, calc(100% - 32px));
|
|
margin: 24px auto 40px;
|
|
}
|
|
.hero {
|
|
background: linear-gradient(135deg, #214d3a 0%, #2f6a54 100%);
|
|
color: #f8f5ee;
|
|
border-radius: 24px;
|
|
padding: 28px;
|
|
box-shadow: 0 20px 40px var(--shadow);
|
|
}
|
|
.hero h1 {
|
|
margin: 0 0 10px;
|
|
font-size: clamp(28px, 5vw, 42px);
|
|
line-height: 1.05;
|
|
}
|
|
.hero p {
|
|
margin: 0;
|
|
max-width: 860px;
|
|
color: rgba(248, 245, 238, 0.85);
|
|
line-height: 1.7;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 1.35fr 0.65fr;
|
|
gap: 18px;
|
|
margin-top: 18px;
|
|
}
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid var(--line);
|
|
border-radius: 22px;
|
|
padding: 22px;
|
|
box-shadow: 0 14px 30px var(--shadow);
|
|
}
|
|
.panel h2 {
|
|
margin: 0 0 16px;
|
|
font-size: 20px;
|
|
}
|
|
.section {
|
|
border: 1px solid var(--line);
|
|
border-radius: 18px;
|
|
padding: 16px;
|
|
background: #fffdf8;
|
|
}
|
|
.section + .section {
|
|
margin-top: 14px;
|
|
}
|
|
.section h3 {
|
|
margin: 0 0 12px;
|
|
font-size: 16px;
|
|
}
|
|
.status-line {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
border-radius: 999px;
|
|
background: var(--panel-strong);
|
|
border: 1px solid var(--line);
|
|
font-weight: 600;
|
|
}
|
|
.pill.running {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-color: rgba(32, 92, 67, 0.2);
|
|
}
|
|
.note {
|
|
margin: 10px 0 0;
|
|
color: var(--muted);
|
|
line-height: 1.7;
|
|
}
|
|
.flash {
|
|
padding: 12px 14px;
|
|
border-radius: 16px;
|
|
margin-bottom: 14px;
|
|
line-height: 1.6;
|
|
border: 1px solid transparent;
|
|
}
|
|
.flash.ok {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-color: rgba(32, 92, 67, 0.18);
|
|
}
|
|
.flash.err {
|
|
background: var(--warn-soft);
|
|
color: var(--warn);
|
|
border-color: rgba(138, 59, 47, 0.18);
|
|
}
|
|
form {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
.fields {
|
|
display: grid;
|
|
gap: 14px;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
.fields .wide {
|
|
grid-column: 1 / -1;
|
|
}
|
|
label {
|
|
display: grid;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
label span {
|
|
color: var(--muted);
|
|
font-weight: 500;
|
|
font-size: 13px;
|
|
}
|
|
input, select {
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
border-radius: 14px;
|
|
border: 1px solid var(--line);
|
|
background: #fffdf8;
|
|
color: var(--ink);
|
|
font: inherit;
|
|
}
|
|
.checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
}
|
|
.checkbox input {
|
|
width: 18px;
|
|
height: 18px;
|
|
margin: 0;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-top: 6px;
|
|
}
|
|
button {
|
|
appearance: none;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 12px 20px;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
.primary {
|
|
background: var(--accent);
|
|
color: #f7f3ec;
|
|
}
|
|
.secondary {
|
|
background: #e7dfd0;
|
|
color: #493c2c;
|
|
}
|
|
.logs {
|
|
margin-top: 18px;
|
|
}
|
|
.logbox {
|
|
border-radius: 18px;
|
|
border: 1px solid var(--line);
|
|
background: #1f2520;
|
|
color: #dbe5dc;
|
|
padding: 16px;
|
|
min-height: 320px;
|
|
max-height: 560px;
|
|
overflow: auto;
|
|
font-family: Consolas, "Courier New", monospace;
|
|
font-size: 13px;
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.empty {
|
|
color: #9cab9e;
|
|
}
|
|
code {
|
|
font-family: Consolas, "Courier New", monospace;
|
|
}
|
|
@media (max-width: 900px) {
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.fields {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="shell">
|
|
<section class="hero">
|
|
<h1>UNC 文件监听控制台</h1>
|
|
<p>支持两类规则同时运行。压缩包规则专门处理 <code>.tar.gz</code> 并结合 zip / unzip 目录去重;普通文件规则按扩展名列表把文件从 UNC 路径复制或剪切到本机目录,并支持按“同名+大小”或“同名+哈希”去重。</p>
|
|
</section>
|
|
|
|
<section class="grid">
|
|
<div class="panel">
|
|
<h2>监听配置</h2>
|
|
{{if .SubmitMessage}}<div class="flash ok">{{.SubmitMessage}}</div>{{end}}
|
|
{{if .SubmitError}}<div class="flash err">{{.SubmitError}}</div>{{end}}
|
|
{{if .LastError}}<div class="flash err">{{.LastError}}</div>{{end}}
|
|
<form method="post" action="/start">
|
|
<div class="section">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="archive_enabled" {{if .Form.ArchiveEnabled}}checked{{end}}>
|
|
启用压缩包规则
|
|
</label>
|
|
<div class="fields">
|
|
<label class="wide">压缩包 UNC 源路径
|
|
<span>例如 \\server\share\incoming,仅处理 .tar.gz</span>
|
|
<input name="archive_source" value="{{.Form.ArchiveSourceDir}}" placeholder="\\server\share\incoming">
|
|
</label>
|
|
<label>zip 路径
|
|
<span>需要复制或剪切到这里</span>
|
|
<input name="archive_zip" value="{{.Form.ArchiveZipDir}}" placeholder="D:\zip">
|
|
</label>
|
|
<label>unzip 路径
|
|
<span>已有同名解压目录则跳过</span>
|
|
<input name="archive_unzip" value="{{.Form.ArchiveUnzipDir}}" placeholder="D:\unzip">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="generic_enabled" {{if .Form.GenericEnabled}}checked{{end}}>
|
|
启用普通文件规则
|
|
</label>
|
|
<div class="fields">
|
|
<label class="wide">普通文件 UNC 源路径
|
|
<span>例如 \\server\share\others</span>
|
|
<input name="generic_source" value="{{.Form.GenericSourceDir}}" placeholder="\\server\share\others">
|
|
</label>
|
|
<label>本机目标路径
|
|
<span>只处理到这个目录,不检查 unzip</span>
|
|
<input name="generic_target" value="{{.Form.GenericTargetDir}}" placeholder="D:\other_files">
|
|
</label>
|
|
<label>扩展名列表
|
|
<span>例如 .csv,.txt,.pdf 或 xlsx,docx</span>
|
|
<input name="generic_exts" value="{{.Form.GenericExts}}" placeholder=".csv,.txt,.pdf">
|
|
</label>
|
|
<label>去重方式
|
|
<span>目标目录已有同名文件时使用</span>
|
|
<select name="generic_dedupe">
|
|
<option value="name_size" {{if eq .Form.GenericDedupe "name_size"}}selected{{end}}>同名且大小相同才跳过</option>
|
|
<option value="name_hash" {{if eq .Form.GenericDedupe "name_hash"}}selected{{end}}>同名且哈希相同才跳过</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>公共设置</h3>
|
|
<div class="fields">
|
|
<label>扫描间隔
|
|
<span>支持 Go duration,如 15s、1m</span>
|
|
<input name="interval" value="{{.Form.Interval}}" placeholder="15s">
|
|
</label>
|
|
<label>稳定时长
|
|
<span>最后修改时间至少早于当前多久</span>
|
|
<input name="stable_for" value="{{.Form.StableFor}}" placeholder="30s">
|
|
</label>
|
|
<label>稳定扫描次数
|
|
<span>连续几次扫描未变化才处理</span>
|
|
<input name="stable_scans" value="{{.Form.StableScans}}" placeholder="2">
|
|
</label>
|
|
<label>处理方式
|
|
<span>copy 复制,move 剪切</span>
|
|
<select name="mode">
|
|
<option value="copy" {{if eq .Form.Mode "copy"}}selected{{end}}>copy</option>
|
|
<option value="move" {{if eq .Form.Mode "move"}}selected{{end}}>move</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<label class="checkbox" style="margin-top:12px;">
|
|
<input type="checkbox" name="recursive" {{if .Form.Recursive}}checked{{end}}>
|
|
递归扫描源目录
|
|
</label>
|
|
</div>
|
|
|
|
<p class="note">页面配置会自动保存到 <code>{{.SettingsPath}}</code>,下次打开页面会自动带出。</p>
|
|
|
|
<div class="actions">
|
|
<button class="primary" type="submit">启动或重启监听</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>运行状态</h2>
|
|
<div class="status-line">
|
|
<div class="pill {{if .Running}}running{{end}}">
|
|
{{if .Running}}运行中{{else}}未运行{{end}}
|
|
</div>
|
|
{{if .StartedAt}}
|
|
<div class="pill">启动时间 {{.StartedAt}}</div>
|
|
{{end}}
|
|
</div>
|
|
{{if .LastMessage}}
|
|
<p class="note">{{.LastMessage}}</p>
|
|
{{end}}
|
|
<p class="note">如果 UNC 目录中的文件由后台服务持续写入,建议把“稳定时长”和“稳定扫描次数”设得更保守一些,例如 <code>stable_for=1m</code>、<code>stable_scans=3</code>。</p>
|
|
<p class="note">普通文件规则里如果目标目录出现同名但不同大小,或者同名但哈希不同,程序会记录为冲突并跳过,不会覆盖已有文件。</p>
|
|
<form method="post" action="/stop">
|
|
<div class="actions">
|
|
<button class="secondary" type="submit">停止监听</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel logs">
|
|
<h2>最近日志</h2>
|
|
<div class="logbox">{{if .Logs}}{{range .Logs}}{{.}}
|
|
{{end}}{{else}}<span class="empty">暂无日志。启动监听后,这里会显示文件发现、跳过、复制、剪切和错误信息。</span>{{end}}</div>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>`))
|