添加 上传接口
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
.feishu_oauth_tokens.json
|
||||
inventory_sync/__pycache__
|
||||
.DS_Store
|
||||
batch_inventory_data/*
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local inventory file upload page.
|
||||
|
||||
Run:
|
||||
python3 upload_inventory_page.py
|
||||
|
||||
Then open:
|
||||
http://127.0.0.1:8080
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cgi
|
||||
from datetime import datetime
|
||||
from html import escape
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from typing import Iterable
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 8080
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
UPLOAD_DIR = PROJECT_ROOT / "batch_inventory_data"
|
||||
MAX_UPLOAD_BYTES = 200 * 1024 * 1024
|
||||
|
||||
|
||||
def safe_filename(filename: str) -> str:
|
||||
name = Path(filename or "inventory_file").name.strip()
|
||||
name = re.sub(r"[^\w.\-\u4e00-\u9fff()() ]+", "_", name)
|
||||
name = re.sub(r"\s+", "_", name)
|
||||
return name or "inventory_file"
|
||||
|
||||
|
||||
def unique_upload_path(filename: str) -> Path:
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
base_name = safe_filename(filename)
|
||||
candidate = UPLOAD_DIR / f"{timestamp}_{base_name}"
|
||||
counter = 1
|
||||
while candidate.exists():
|
||||
candidate = UPLOAD_DIR / f"{timestamp}_{counter}_{base_name}"
|
||||
counter += 1
|
||||
return candidate
|
||||
|
||||
|
||||
def list_uploaded_files(limit: int = 20) -> list[Path]:
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
files = [path for path in UPLOAD_DIR.iterdir() if path.is_file() and not path.name.startswith(".")]
|
||||
return sorted(files, key=lambda item: item.stat().st_mtime, reverse=True)[:limit]
|
||||
|
||||
|
||||
def human_size(size: int) -> str:
|
||||
units = ["B", "KB", "MB", "GB"]
|
||||
value = float(size)
|
||||
for unit in units:
|
||||
if value < 1024 or unit == units[-1]:
|
||||
return f"{value:.1f} {unit}" if unit != "B" else f"{int(value)} {unit}"
|
||||
value /= 1024
|
||||
return f"{size} B"
|
||||
|
||||
|
||||
def render_recent_files(files: Iterable[Path]) -> str:
|
||||
rows = []
|
||||
for path in files:
|
||||
stat = path.stat()
|
||||
uploaded_at = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td>{escape(path.name)}</td>"
|
||||
f"<td>{escape(human_size(stat.st_size))}</td>"
|
||||
f"<td>{escape(uploaded_at)}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
if not rows:
|
||||
return '<p class="empty">还没有上传文件。上传成功后,最近文件会显示在这里。</p>'
|
||||
return (
|
||||
'<table class="file-table">'
|
||||
"<thead><tr><th>文件名</th><th>大小</th><th>上传时间</th></tr></thead>"
|
||||
f"<tbody>{''.join(rows)}</tbody>"
|
||||
"</table>"
|
||||
)
|
||||
|
||||
|
||||
def render_page(message: str = "", is_error: bool = False) -> bytes:
|
||||
message_html = ""
|
||||
if message:
|
||||
message_class = "message error" if is_error else "message success"
|
||||
message_html = f'<div class="{message_class}">{escape(message)}</div>'
|
||||
|
||||
html = f"""<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>委外批次库存文件上传</title>
|
||||
<style>
|
||||
:root {{
|
||||
--ink: #101828;
|
||||
--muted: #667085;
|
||||
--line: #e4e7ec;
|
||||
--surface: #ffffff;
|
||||
--soft: #f7f4ec;
|
||||
--dark: #172033;
|
||||
--accent: #087f5b;
|
||||
--accent-2: #b7791f;
|
||||
--accent-soft: #ecfdf3;
|
||||
--danger: #b42318;
|
||||
--shadow: 0 24px 70px rgba(16, 24, 40, .14);
|
||||
}}
|
||||
|
||||
* {{
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 8% 8%, rgba(183, 121, 31, .16), transparent 24rem),
|
||||
radial-gradient(circle at 92% 6%, rgba(8, 127, 91, .14), transparent 26rem),
|
||||
linear-gradient(180deg, #f8fafc 0%, #f3f0e8 100%);
|
||||
}}
|
||||
|
||||
main {{
|
||||
width: min(1180px, calc(100% - 36px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 48px;
|
||||
}}
|
||||
|
||||
.shell {{
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(228, 231, 236, .92);
|
||||
border-radius: 34px;
|
||||
background: rgba(255, 255, 255, .74);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(18px);
|
||||
}}
|
||||
|
||||
.header {{
|
||||
position: relative;
|
||||
padding: 20px 26px;
|
||||
color: #fff;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(23, 32, 51, .98), rgba(15, 68, 65, .95)),
|
||||
radial-gradient(circle at 88% 20%, rgba(183, 121, 31, .42), transparent 18rem);
|
||||
}}
|
||||
|
||||
.header::after {{
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -70px;
|
||||
bottom: -150px;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
border-radius: 50%;
|
||||
box-shadow: -42px -26px 0 rgba(255, 255, 255, .05);
|
||||
}}
|
||||
|
||||
.nav {{
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}}
|
||||
|
||||
.brand {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .08em;
|
||||
}}
|
||||
|
||||
.brand-icon {{
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
color: var(--dark);
|
||||
background: #f2c46d;
|
||||
font-weight: 900;
|
||||
}}
|
||||
|
||||
.usage {{
|
||||
padding: 7px 11px;
|
||||
border: 1px solid rgba(255, 255, 255, .16);
|
||||
border-radius: 999px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}}
|
||||
|
||||
.title-row {{
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
}}
|
||||
|
||||
h1 {{
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
font-size: clamp(28px, 4vw, 42px);
|
||||
line-height: 1.04;
|
||||
letter-spacing: -1.5px;
|
||||
}}
|
||||
|
||||
.subtitle {{
|
||||
max-width: 720px;
|
||||
margin: 10px 0 0;
|
||||
color: rgba(255, 255, 255, .74);
|
||||
font-size: 15px;
|
||||
line-height: 1.58;
|
||||
}}
|
||||
|
||||
.storage-card {{
|
||||
width: min(330px, 100%);
|
||||
padding: 13px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, .14);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, .09);
|
||||
}}
|
||||
|
||||
.storage-card span {{
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(255, 255, 255, .58);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .12em;
|
||||
}}
|
||||
|
||||
.storage-card strong {{
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}}
|
||||
|
||||
.workspace {{
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, .65fr);
|
||||
gap: 24px;
|
||||
padding: 26px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 250, 252, .96), rgba(247, 244, 236, .96));
|
||||
}}
|
||||
|
||||
.drop-panel, .side-panel, .recent {{
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
background: var(--surface);
|
||||
box-shadow: 0 16px 44px rgba(16, 24, 40, .08);
|
||||
}}
|
||||
|
||||
.drop-panel {{
|
||||
padding: 18px;
|
||||
}}
|
||||
|
||||
.message {{
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
font-weight: 800;
|
||||
}}
|
||||
|
||||
.success {{
|
||||
color: #05603a;
|
||||
background: #ecfdf3;
|
||||
border: 1px solid #abefc6;
|
||||
}}
|
||||
|
||||
.error {{
|
||||
color: var(--danger);
|
||||
background: #fef3f2;
|
||||
border: 1px solid #fecdca;
|
||||
}}
|
||||
|
||||
.dropzone {{
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 470px;
|
||||
padding: 44px;
|
||||
border: 2px dashed #98a2b3;
|
||||
border-radius: 24px;
|
||||
text-align: center;
|
||||
background:
|
||||
linear-gradient(#fff, #fff) padding-box,
|
||||
repeating-linear-gradient(45deg, rgba(8, 127, 91, .08) 0 10px, rgba(183, 121, 31, .08) 10px 20px);
|
||||
transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, background .18s ease;
|
||||
}}
|
||||
|
||||
.dropzone.dragging {{
|
||||
border-color: var(--accent);
|
||||
background:
|
||||
linear-gradient(#f0fdf4, #ffffff) padding-box,
|
||||
repeating-linear-gradient(45deg, rgba(8, 127, 91, .16) 0 10px, rgba(183, 121, 31, .10) 10px 20px);
|
||||
box-shadow: inset 0 0 0 8px rgba(8, 127, 91, .08);
|
||||
transform: scale(.992);
|
||||
}}
|
||||
|
||||
.drop-icon {{
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
margin: 0 auto 26px;
|
||||
border-radius: 32px;
|
||||
color: #fff;
|
||||
background:
|
||||
linear-gradient(135deg, var(--accent), #0f5f56 62%, var(--accent-2));
|
||||
box-shadow: 0 18px 40px rgba(8, 127, 91, .28);
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
}}
|
||||
|
||||
.drop-kicker {{
|
||||
margin-bottom: 12px;
|
||||
color: var(--accent-2);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: .18em;
|
||||
}}
|
||||
|
||||
.dropzone h2 {{
|
||||
margin: 0;
|
||||
font-size: clamp(30px, 4vw, 48px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -1.6px;
|
||||
}}
|
||||
|
||||
.dropzone p {{
|
||||
max-width: 520px;
|
||||
margin: 16px auto 0;
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
}}
|
||||
|
||||
.drop-status {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 220px;
|
||||
margin-top: 28px;
|
||||
padding: 13px 18px;
|
||||
border-radius: 999px;
|
||||
color: #344054;
|
||||
background: #f9fafb;
|
||||
border: 1px solid var(--line);
|
||||
font-weight: 900;
|
||||
}}
|
||||
|
||||
.dropzone.dragging .drop-status {{
|
||||
color: #05603a;
|
||||
background: #dcfae6;
|
||||
border-color: #75e0a7;
|
||||
}}
|
||||
|
||||
.side-panel {{
|
||||
padding: 24px;
|
||||
}}
|
||||
|
||||
.side-panel h2, .recent h2 {{
|
||||
margin: 0 0 16px;
|
||||
font-size: 22px;
|
||||
letter-spacing: -.4px;
|
||||
}}
|
||||
|
||||
.rule-list {{
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}}
|
||||
|
||||
.rule-list li {{
|
||||
display: grid;
|
||||
grid-template-columns: 34px 1fr;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: #fcfcfd;
|
||||
}}
|
||||
|
||||
.rule-index {{
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: var(--dark);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}}
|
||||
|
||||
.rule-list strong {{
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 15px;
|
||||
}}
|
||||
|
||||
.rule-list span {{
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}}
|
||||
|
||||
.recent {{
|
||||
grid-column: 1 / -1;
|
||||
padding: 24px;
|
||||
}}
|
||||
|
||||
.file-table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}}
|
||||
|
||||
th {{
|
||||
color: #344054;
|
||||
background: #f9fafb;
|
||||
font-size: 12px;
|
||||
letter-spacing: .08em;
|
||||
}}
|
||||
|
||||
.empty {{
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}}
|
||||
|
||||
@media (max-width: 900px) {{
|
||||
main {{
|
||||
width: min(100% - 24px, 760px);
|
||||
padding: 22px 0 36px;
|
||||
}}
|
||||
|
||||
.header {{
|
||||
padding: 20px;
|
||||
}}
|
||||
|
||||
.nav, .title-row {{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}}
|
||||
|
||||
.nav {{
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
.workspace {{
|
||||
grid-template-columns: 1fr;
|
||||
padding: 18px;
|
||||
}}
|
||||
|
||||
.dropzone {{
|
||||
min-height: 380px;
|
||||
padding: 28px;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="shell">
|
||||
<section class="header">
|
||||
<div class="nav">
|
||||
<div class="brand"><span class="brand-icon">委</span><span>委外批次库存上传台</span></div>
|
||||
<div class="usage">每周约 2 次使用</div>
|
||||
</div>
|
||||
<div class="title-row">
|
||||
<div>
|
||||
<h1>投递委外批次库存文件</h1>
|
||||
<p class="subtitle">拖入委外仓、加工方或批次盘点文件,系统会按上传时间自动归档,便于后续同步、核对和追溯。</p>
|
||||
</div>
|
||||
<div class="storage-card">
|
||||
<span>ARCHIVE PATH</span>
|
||||
<strong>{escape(str(UPLOAD_DIR))}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workspace">
|
||||
<div class="drop-panel">
|
||||
{message_html}
|
||||
<div class="dropzone" id="dropzone">
|
||||
<div>
|
||||
<div class="drop-icon">⇣</div>
|
||||
<div class="drop-kicker">DROP FILE HERE</div>
|
||||
<h2>拖入文件,立即上传</h2>
|
||||
<p>支持 Excel、CSV 等委外批次库存文件。松开鼠标后自动保存到 batch_inventory_data,无需点击选择文件。</p>
|
||||
<div class="drop-status" id="dropStatus">等待拖入文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="side-panel">
|
||||
<h2>上传规范</h2>
|
||||
<ul class="rule-list">
|
||||
<li><span class="rule-index">01</span><div><strong>文件类型</strong><span>建议上传 Excel 或 CSV 格式的委外批次库存文件。</span></div></li>
|
||||
<li><span class="rule-index">02</span><div><strong>文件命名</strong><span>系统会自动追加上传时间,不需要手动改名。</span></div></li>
|
||||
<li><span class="rule-index">03</span><div><strong>大小限制</strong><span>单文件最大 {escape(human_size(MAX_UPLOAD_BYTES))}。</span></div></li>
|
||||
<li><span class="rule-index">04</span><div><strong>保存位置</strong><span>上传后统一进入 batch_inventory_data 目录。</span></div></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section class="recent">
|
||||
<h2>最近上传的委外批次文件</h2>
|
||||
{render_recent_files(list_uploaded_files())}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const dropzone = document.getElementById("dropzone");
|
||||
const dropStatus = document.getElementById("dropStatus");
|
||||
|
||||
function stopDefaults(event) {{
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {{
|
||||
document.addEventListener(eventName, stopDefaults);
|
||||
}});
|
||||
|
||||
["dragenter", "dragover"].forEach((eventName) => {{
|
||||
dropzone.addEventListener(eventName, () => {{
|
||||
dropzone.classList.add("dragging");
|
||||
dropStatus.textContent = "松开鼠标开始上传";
|
||||
}});
|
||||
}});
|
||||
|
||||
["dragleave", "drop"].forEach((eventName) => {{
|
||||
dropzone.addEventListener(eventName, () => {{
|
||||
dropzone.classList.remove("dragging");
|
||||
}});
|
||||
}});
|
||||
|
||||
dropzone.addEventListener("drop", async (event) => {{
|
||||
const files = event.dataTransfer && event.dataTransfer.files;
|
||||
if (!files || !files.length) {{
|
||||
dropStatus.textContent = "没有检测到文件,请重新拖入。";
|
||||
return;
|
||||
}}
|
||||
|
||||
dropStatus.textContent = `上传中:${{files[0].name}}`;
|
||||
const formData = new FormData();
|
||||
formData.append("inventory_file", files[0], files[0].name);
|
||||
|
||||
try {{
|
||||
const response = await fetch("/upload", {{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}});
|
||||
window.location.href = response.url || "/";
|
||||
}} catch (error) {{
|
||||
dropStatus.textContent = "上传失败,请重试。";
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html.encode("utf-8")
|
||||
|
||||
|
||||
class UploadHandler(BaseHTTPRequestHandler):
|
||||
server_version = "InventoryUpload/1.0"
|
||||
|
||||
def do_GET(self) -> None:
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/favicon.ico":
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
return
|
||||
if parsed.path != "/":
|
||||
self.send_error(404, "Not Found")
|
||||
return
|
||||
|
||||
params = parse_qs(parsed.query)
|
||||
message = params.get("message", [""])[0]
|
||||
is_error = params.get("error", ["0"])[0] == "1"
|
||||
self.respond_html(render_page(message, is_error))
|
||||
|
||||
def do_POST(self) -> None:
|
||||
path = urlparse(self.path).path
|
||||
if path != "/upload":
|
||||
self.send_error(404, "Not Found")
|
||||
return
|
||||
|
||||
content_length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
if content_length <= 0:
|
||||
self.redirect("/", "没有收到上传文件。", is_error=True)
|
||||
return
|
||||
if content_length > MAX_UPLOAD_BYTES:
|
||||
self.redirect("/", f"文件太大,最大支持 {human_size(MAX_UPLOAD_BYTES)}。", is_error=True)
|
||||
return
|
||||
|
||||
form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={
|
||||
"REQUEST_METHOD": "POST",
|
||||
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
|
||||
"CONTENT_LENGTH": str(content_length),
|
||||
})
|
||||
file_item = form["inventory_file"] if "inventory_file" in form else None
|
||||
if file_item is None or not getattr(file_item, "filename", ""):
|
||||
self.redirect("/", "请选择需要上传的文件。", is_error=True)
|
||||
return
|
||||
|
||||
target_path = unique_upload_path(file_item.filename)
|
||||
with target_path.open("wb") as output:
|
||||
shutil.copyfileobj(file_item.file, output)
|
||||
|
||||
self.redirect("/", f"上传成功:{target_path.name}")
|
||||
|
||||
def respond_html(self, body: bytes) -> None:
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def redirect(self, path: str, message: str, is_error: bool = False) -> None:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
query = urlencode({"message": message, "error": "1" if is_error else "0"})
|
||||
self.send_response(303)
|
||||
self.send_header("Location", f"{path}?{query}")
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{timestamp}] {self.address_string()} - {format % args}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
server = ThreadingHTTPServer((HOST, PORT), UploadHandler)
|
||||
print(f"上传页面已启动: http://{HOST}:{PORT}")
|
||||
print(f"文件保存目录: {UPLOAD_DIR}")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n上传页面已停止。")
|
||||
finally:
|
||||
server.server_close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user