201 lines
7.5 KiB
Python
201 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""简易文件上传服务 - 浏览器打开即可拖拽上传"""
|
|
import os
|
|
import cgi
|
|
import html
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
|
|
UPLOAD_DIR = os.path.expanduser("~/.openclaw/workspace-xiaoban/tmp/uploads")
|
|
PORT = 18888
|
|
|
|
HTML = r"""<!DOCTYPE html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>上传文件到大麦</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
|
.container{text-align:center;max-width:600px;padding:40px}
|
|
h1{font-size:28px;margin-bottom:8px}
|
|
.sub{color:#94a3b8;margin-bottom:32px;font-size:14px}
|
|
.dropzone{background:#1e293b;border:2px dashed #475569;border-radius:16px;padding:60px 20px;cursor:pointer;transition:all .2s}
|
|
.dropzone:hover,.dropzone.dragover{border-color:#6366f1;background:#1e1b4b}
|
|
.dropzone p{font-size:16px;color:#94a3b8}
|
|
.dropzone .icon{font-size:48px;margin-bottom:16px}
|
|
input[type=file]{display:none}
|
|
#progress{display:none;margin-top:16px}
|
|
.bar-bg{background:#334155;border-radius:8px;height:8px;overflow:hidden}
|
|
.bar{background:linear-gradient(90deg,#6366f1,#a855f7);height:100%;width:0;transition:width .3s}
|
|
#result{margin-top:20px;font-size:14px}
|
|
.success{color:#4ade80}
|
|
.error{color:#f87171}
|
|
.recent{margin-top:32px;text-align:left;max-height:200px;overflow-y:auto}
|
|
.recent h3{font-size:14px;color:#64748b;margin-bottom:8px}
|
|
.recent li{font-size:12px;color:#94a3b8;padding:4px 0;border-bottom:1px solid #1e293b}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>📤 上传文件到大麦</h1>
|
|
<p class="sub">拖拽文件到下方区域 或 点击选择文件</p>
|
|
<div class="dropzone" id="dropzone" onclick="document.getElementById('file').click()">
|
|
<div class="icon">📁</div>
|
|
<p>点击或拖拽文件到此处上传</p>
|
|
</div>
|
|
<input type="file" id="file" multiple onchange="upload(this.files)">
|
|
<div id="progress">
|
|
<p id="status">上传中...</p>
|
|
<div class="bar-bg"><div class="bar" id="bar"></div></div>
|
|
</div>
|
|
<div id="result"></div>
|
|
<div class="recent" id="recent"><h3>📋 最近上传</h3><ul id="filelist"></ul></div>
|
|
</div>
|
|
<script>
|
|
const dropzone = document.getElementById('dropzone');
|
|
['dragenter','dragover','dragleave','drop'].forEach(e => {
|
|
dropzone.addEventListener(e, ev => { ev.preventDefault(); ev.stopPropagation(); });
|
|
});
|
|
['dragenter','dragover'].forEach(e => dropzone.addEventListener(e, () => dropzone.classList.add('dragover')));
|
|
['dragleave','drop'].forEach(e => dropzone.addEventListener(e, () => dropzone.classList.remove('dragover')));
|
|
dropzone.addEventListener('drop', ev => upload(ev.dataTransfer.files));
|
|
|
|
async function upload(files){
|
|
if(!files.length) return;
|
|
const progress = document.getElementById('progress');
|
|
const bar = document.getElementById('bar');
|
|
const status = document.getElementById('status');
|
|
const result = document.getElementById('result');
|
|
progress.style.display='block'; bar.style.width='0'; result.innerHTML='';
|
|
|
|
let ok=0, fail=0;
|
|
for(let i=0;i<files.length;i++){
|
|
const f = files[i];
|
|
status.textContent = `正在上传: ${f.name} (${(f.size/1024/1024).toFixed(1)}MB)...`;
|
|
bar.style.width = ((i/files.length)*100)+'%';
|
|
const fd = new FormData(); fd.append('file', f);
|
|
try{
|
|
const r = await fetch('/upload', {method:'POST', body:fd});
|
|
const t = await r.text();
|
|
if(r.ok){ ok++; } else { fail++; result.innerHTML+=`<div class="error">❌ ${f.name}: ${t}</div>`; }
|
|
}catch(e){ fail++; result.innerHTML+=`<div class="error">❌ ${f.name}: ${e}</div>`; }
|
|
}
|
|
bar.style.width='100%';
|
|
if(fail===0) result.innerHTML+=`<div class="success">✅ 全部 ${ok} 个文件上传成功!</div>`;
|
|
else result.innerHTML+=`<div class="success">✅ ${ok} 个成功</div><div class="error">❌ ${fail} 个失败</div>`;
|
|
status.textContent='上传完成!';
|
|
loadFiles();
|
|
setTimeout(()=>{ progress.style.display='none'; result.innerHTML=''; }, 5000);
|
|
}
|
|
|
|
async function loadFiles(){
|
|
try{
|
|
const r = await fetch('/files');
|
|
const files = await r.json();
|
|
document.getElementById('filelist').innerHTML = files.map(f =>
|
|
`<li>📄 ${f.name} <span style="color:#64748b">${(f.size/1024/1024).toFixed(1)}MB · ${f.time}</span></li>`
|
|
).join('');
|
|
}catch(e){}
|
|
}
|
|
loadFiles();
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
class UploadHandler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == '/':
|
|
self._send_html(HTML)
|
|
elif self.path == '/files':
|
|
self._list_files()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def do_POST(self):
|
|
if self.path == '/upload':
|
|
self._handle_upload()
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def _send_html(self, content, code=200):
|
|
body = content.encode('utf-8')
|
|
self.send_response(code)
|
|
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
self.send_header('Content-Length', len(body))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def _list_files(self):
|
|
import json
|
|
import time
|
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
files = []
|
|
for name in sorted(os.listdir(UPLOAD_DIR), reverse=True):
|
|
path = os.path.join(UPLOAD_DIR, name)
|
|
if os.path.isfile(path):
|
|
st = os.stat(path)
|
|
files.append({
|
|
'name': name,
|
|
'size': st.st_size,
|
|
'time': time.strftime('%m-%d %H:%M', time.localtime(st.st_mtime))
|
|
})
|
|
body = json.dumps(files[:20]).encode('utf-8')
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Content-Length', len(body))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def _handle_upload(self):
|
|
content_type = self.headers.get('Content-Type', '')
|
|
if 'multipart/form-data' not in content_type:
|
|
self.send_error(400, '需要 multipart/form-data')
|
|
return
|
|
|
|
form = cgi.FieldStorage(
|
|
fp=self.rfile,
|
|
headers=self.headers,
|
|
environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': content_type}
|
|
)
|
|
|
|
file_item = form['file']
|
|
if not file_item.filename:
|
|
self.send_error(400, '未选择文件')
|
|
return
|
|
|
|
safe_name = os.path.basename(file_item.filename)
|
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
path = os.path.join(UPLOAD_DIR, safe_name)
|
|
|
|
# 避免覆盖,自动加序号
|
|
base, ext = os.path.splitext(safe_name)
|
|
counter = 1
|
|
while os.path.exists(path):
|
|
path = os.path.join(UPLOAD_DIR, f"{base}_{counter}{ext}")
|
|
counter += 1
|
|
|
|
with open(path, 'wb') as f:
|
|
f.write(file_item.file.read())
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/plain; charset=utf-8')
|
|
self.end_headers()
|
|
self.wfile.write(f'OK: {os.path.basename(path)}'.encode('utf-8'))
|
|
|
|
def log_message(self, format, *args):
|
|
pass # 静默日志
|
|
|
|
|
|
if __name__ == '__main__':
|
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
server = HTTPServer(('0.0.0.0', PORT), UploadHandler)
|
|
print(f'上传服务已启动: http://115.190.225.235:{PORT}')
|
|
print(f'上传目录: {UPLOAD_DIR}')
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print('\n已停止')
|
|
server.server_close()
|