Satur@it-depot.ru 2025-03-05 16:28:54 +03:00
parent 6851b39c28
commit ac5eb45787
3 changed files with 84 additions and 226 deletions

View File

@ -1,9 +1,17 @@
# /opt/rustdesk-organizer/Dockerfile
FROM python:3.11-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY app.py . COPY app.py .
COPY templates/ ./templates/ COPY templates/ ./templates/
EXPOSE 8001
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8001"] # Экспонируем оба порта
EXPOSE 8001 8002
# Запускаем два процесса Uvicorn для разных портов
CMD ["sh", "-c", "uvicorn app:app_get --host 0.0.0.0 --port 8001 & uvicorn app:app_post --host 0.0.0.0 --port 8002"]

292
app.py
View File

@ -1,30 +1,23 @@
from fastapi import FastAPI, HTTPException, File, UploadFile, Query from fastapi import FastAPI, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
import sqlite3 import sqlite3
import datetime import datetime
import csv
import markdown
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse
import io from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() # Создаем два экземпляра FastAPI для разных портов (опционально, для демонстрации)
app_get = FastAPI(title="RustDesk Organizer - Web Interface")
app_post = FastAPI(title="RustDesk Organizer - API")
# Монтируем папку templates как статические файлы для GET
app_get.mount("/templates", StaticFiles(directory="templates"), name="templates")
# Соединение с базой данных # Соединение с базой данных
conn = sqlite3.connect("/db/rustdesk.db", check_same_thread=False) conn = sqlite3.connect("/db/rustdesk.db", check_same_thread=False)
cursor = conn.cursor() cursor = conn.cursor()
# Проверяем и обновляем структуру таблицы installs # Создаем таблицы (оставляем как есть)
cursor.execute("PRAGMA table_info(installs)")
columns = [row[1] for row in cursor.fetchall()]
if 'protocol' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN protocol TEXT DEFAULT 'rustdesk'")
conn.commit()
if 'note' not in columns:
cursor.execute("ALTER TABLE installs ADD COLUMN note TEXT DEFAULT ''")
conn.commit()
# Создаем таблицы (добавляем поле protocol и note, если таблицы нет)
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS folders ( CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -40,24 +33,12 @@ CREATE TABLE IF NOT EXISTS installs (
computer_name TEXT, computer_name TEXT,
install_time TEXT, install_time TEXT,
folder_id INTEGER, folder_id INTEGER,
protocol TEXT DEFAULT 'rustdesk',
note TEXT DEFAULT '',
FOREIGN KEY (folder_id) REFERENCES folders(id) FOREIGN KEY (folder_id) REFERENCES folders(id)
) )
""") """)
conn.commit() conn.commit()
# Проверяем/создаем папку "Несортированные" и получаем её ID # Модели данных (оставляем как есть)
cursor.execute("SELECT id FROM folders WHERE name = 'Несортированные'")
unsorted_folder = cursor.fetchone()
if not unsorted_folder:
cursor.execute("INSERT INTO folders (name) VALUES ('Несортированные')")
conn.commit()
unsorted_folder_id = cursor.lastrowid
else:
unsorted_folder_id = unsorted_folder[0]
# Модели данных
class Folder(BaseModel): class Folder(BaseModel):
name: str name: str
parent_id: int | None = None parent_id: int | None = None
@ -66,246 +47,111 @@ class FolderUpdate(BaseModel):
name: str name: str
class InstallData(BaseModel): class InstallData(BaseModel):
rust_id: str | None = None rust_id: str
computer_name: str | None = None computer_name: str
install_time: str | None = None # Принимаем строку в формате YYYY-MM-DD HH:MM:SS install_time: str | None = None
folder_id: int | None = None folder_id: int | None = None
protocol: str | None = 'rustdesk' # По умолчанию RustDesk
note: str | None = '' # Новая заметка, по умолчанию пустая
# Монтируем папки templates и icons как статические файлы # Функция для работы с базой данных
app.mount("/templates", StaticFiles(directory="templates"), name="templates") def get_db():
app.mount("/icons", StaticFiles(directory="templates/icons"), name="icons") return conn, cursor
# Главная страница # --- GET API (порт 8001) ---
@app.get("/") @app_get.get("/")
async def root(): async def root():
return FileResponse("templates/index.html") return FileResponse("templates/index.html")
# --- Folders API --- @app_get.get("/api/folders")
@app.get("/api/folders")
def get_folders(): def get_folders():
conn, cursor = get_db()
cursor.execute("SELECT * FROM folders") cursor.execute("SELECT * FROM folders")
rows = cursor.fetchall() rows = cursor.fetchall()
return [{"id": row[0], "name": row[1], "parent_id": row[2]} for row in rows] return [{"id": row[0], "name": row[1], "parent_id": row[2]} for row in rows]
@app.post("/api/folders") @app_get.get("/api/installs")
def get_installs():
conn, cursor = get_db()
cursor.execute("""
SELECT i.id, i.rust_id, i.computer_name, i.install_time, i.folder_id, f.name as folder_name
FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id
""")
rows = cursor.fetchall()
return [{"id": row[0], "rust_id": row[1], "computer_name": row[2],
"install_time": row[3], "folder_id": row[4], "folder_name": row[5]}
for row in rows]
# --- POST API (порт 8002) ---
@app_post.post("/api/folders")
def add_folder(folder: Folder): def add_folder(folder: Folder):
conn, cursor = get_db()
cursor.execute("INSERT INTO folders (name, parent_id) VALUES (?, ?)", cursor.execute("INSERT INTO folders (name, parent_id) VALUES (?, ?)",
(folder.name, folder.parent_id)) (folder.name, folder.parent_id))
conn.commit() conn.commit()
return {"status": "success", "id": cursor.lastrowid} return {"status": "success", "id": cursor.lastrowid}
@app.put("/api/folders/{folder_id}") @app_post.put("/api/folders/{folder_id}")
def update_folder(folder_id: int, folder: FolderUpdate): def update_folder(folder_id: int, folder: FolderUpdate):
conn, cursor = get_db()
cursor.execute("UPDATE folders SET name = ? WHERE id = ?", (folder.name, folder_id)) cursor.execute("UPDATE folders SET name = ? WHERE id = ?", (folder.name, folder_id))
conn.commit() conn.commit()
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Папка не найдена") raise HTTPException(status_code=404, detail="Папка не найдена")
return {"status": "success"} return {"status": "success"}
@app.delete("/api/folders/{folder_id}") @app_post.delete("/api/folders/{folder_id}")
def delete_folder(folder_id: int): def delete_folder(folder_id: int):
# Проверяем, не является ли папка "Несортированные" conn, cursor = get_db()
cursor.execute("SELECT name FROM folders WHERE id = ?", (folder_id,))
folder_name = cursor.fetchone()
if folder_name and folder_name[0] == 'Несортированные':
raise HTTPException(status_code=403, detail="Папка 'Несортированные' не может быть удалена")
cursor.execute("DELETE FROM folders WHERE id = ?", (folder_id,)) cursor.execute("DELETE FROM folders WHERE id = ?", (folder_id,))
conn.commit() conn.commit()
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Папка не найдена") raise HTTPException(status_code=404, detail="Папка не найдена")
return {"status": "success"} return {"status": "success"}
# --- Installs API --- @app_post.post("/api/install")
@app.get("/api/installs")
def get_installs():
cursor.execute("""
SELECT i.id, i.rust_id, i.computer_name, i.install_time, i.folder_id, f.name as folder_name, i.protocol, i.note
FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id
""")
rows = cursor.fetchall()
# Обработка времени в разных форматах
def format_time(time_str):
if not time_str:
return None
try:
# Пробуем разобрать ISO 8601 (с T и Z)
dt = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
return dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
try:
# Пробуем разобрать наш читаемый формат
dt = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
return dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
return time_str # Возвращаем как есть, если формат не распознан
return [{"id": row[0], "rust_id": row[1], "computer_name": row[2],
"install_time": format_time(row[3]),
"folder_id": row[4], "folder_name": row[5], "protocol": row[6], "note": row[7]}
for row in rows]
@app.post("/api/install")
def add_install(data: InstallData): def add_install(data: InstallData):
install_time = data.install_time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Форматируем в читаемый формат install_time = data.install_time or datetime.datetime.now().isoformat()
folder_id = data.folder_id if data.folder_id is not None else unsorted_folder_id conn, cursor = get_db()
protocol = data.protocol or 'rustdesk' # По умолчанию RustDesk для POST-запросов cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id) VALUES (?, ?, ?, ?)",
note = data.note or '' # Новая заметка, по умолчанию пустая (data.rust_id, data.computer_name, install_time, data.folder_id))
cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note) VALUES (?, ?, ?, ?, ?, ?)",
(data.rust_id, data.computer_name, install_time, folder_id, protocol, note))
conn.commit() conn.commit()
return {"status": "success"} return {"status": "success"}
@app.put("/api/install/{install_id}") @app_post.put("/api/install/{install_id}")
def update_install(install_id: int, data: InstallData): def update_install(install_id: int, data: InstallData):
cursor.execute("SELECT rust_id, computer_name, install_time, folder_id, protocol, note FROM installs WHERE id = ?", (install_id,)) conn, cursor = get_db()
current = cursor.fetchone()
if not current:
raise HTTPException(status_code=404, detail="Запись не найдена")
new_rust_id = data.rust_id if data.rust_id is not None else current[0]
new_computer_name = data.computer_name if data.computer_name is not None else current[1]
new_install_time = data.install_time if data.install_time is not None else current[2]
new_folder_id = data.folder_id if data.folder_id is not None else current[3]
new_protocol = data.protocol if data.protocol is not None else current[4]
new_note = data.note if data.note is not None else current[5]
# Форматируем время, если оно предоставлено в запросе
if new_install_time:
try:
# Проверяем, что время в формате YYYY-MM-DD HH:MM:SS
datetime.datetime.strptime(new_install_time, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(status_code=400, detail="Неверный формат времени. Используйте YYYY-MM-DD HH:MM:SS")
else:
new_install_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(""" cursor.execute("""
UPDATE installs UPDATE installs
SET rust_id = ?, computer_name = ?, install_time = ?, folder_id = ?, protocol = ?, note = ? SET rust_id = ?, computer_name = ?, install_time = ?, folder_id = ?
WHERE id = ? WHERE id = ?
""", (new_rust_id, new_computer_name, new_install_time, new_folder_id, new_protocol, new_note, install_id)) """, (data.rust_id, data.computer_name, data.install_time or datetime.datetime.now().isoformat(),
data.folder_id, install_id))
conn.commit() conn.commit()
if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Запись не найдена")
return {"status": "success"} return {"status": "success"}
@app.delete("/api/install/{install_id}") @app_post.delete("/api/install/{install_id}")
def delete_install(install_id: int): def delete_install(install_id: int):
conn, cursor = get_db()
cursor.execute("DELETE FROM installs WHERE id = ?", (install_id,)) cursor.execute("DELETE FROM installs WHERE id = ?", (install_id,))
conn.commit() conn.commit()
if cursor.rowcount == 0: if cursor.rowcount == 0:
raise HTTPException(status_code=404, detail="Запись не найдена") raise HTTPException(status_code=404, detail="Запись не найдена")
return {"status": "success"} return {"status": "success"}
# --- CSV Export/Import API --- # CORS для обоих приложений
@app.get("/api/export/csv") for app in [app_get, app_post]:
async def export_csv(folder_id: int | None = Query(None, description="ID папки для экспорта, если None - экспортировать все папки")): app.add_middleware(
if folder_id: CORSMiddleware,
cursor.execute(""" allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"],
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note allow_credentials=True,
FROM installs i allow_methods=["*"],
LEFT JOIN folders f ON i.folder_id = f.id allow_headers=["*"],
WHERE i.folder_id = ? )
""", (folder_id,))
else:
cursor.execute("""
SELECT i.rust_id, i.computer_name, i.install_time, f.name as folder_name, i.protocol, i.note
FROM installs i
LEFT JOIN folders f ON i.folder_id = f.id
""")
rows = cursor.fetchall()
output = io.StringIO() if __name__ == "__main__":
writer = csv.writer(output, lineterminator='\n') import uvicorn
writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка']) # Запускаем два процесса Uvicorn для разных портов (опционально, для локального теста)
for row in rows: uvicorn.run(app_get, host="0.0.0.0", port=8001)
# Обработка времени в разных форматах uvicorn.run(app_post, host="0.0.0.0", port=8002)
install_time = format_time(row[2]) if row[2] else ""
writer.writerow([row[0], row[1], install_time, row[3], row[4], row[5]])
headers = {
'Content-Disposition': 'attachment; filename="rustdesk_data.csv"',
'Content-Type': 'text/csv'
}
return StreamingResponse(iter([output.getvalue()]), headers=headers)
def format_time(time_str):
if not time_str:
return None
try:
# Пробуем разобрать ISO 8601 (с T и Z)
dt = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
return dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
try:
# Пробуем разобрать наш читаемый формат
dt = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
return dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
return time_str # Возвращаем как есть, если формат не распознан
@app.post("/api/import/csv")
async def import_csv(file: UploadFile = File(...), folder_id: int | None = Query(None, description="ID папки для импорта, если None - использовать 'Несортированные'")):
try:
contents = await file.read()
csv_data = io.StringIO(contents.decode('utf-8'))
reader = csv.DictReader(csv_data)
target_folder_id = folder_id if folder_id is not None else unsorted_folder_id
for row in reader:
rust_id = row['ID подключения']
computer_name = row['Имя компьютера']
install_time = row['Время установки']
folder_name = row.get('Папка', None)
protocol = row.get('Протокол', 'rustdesk')
note = row.get('Заметка', '')
# Проверяем, существует ли запись с таким rust_id
cursor.execute("SELECT id FROM installs WHERE rust_id = ?", (rust_id,))
if cursor.fetchone():
continue # Пропускаем дублирующуюся запись
# Проверяем и форматируем время
if install_time:
try:
# Пробуем разобрать время в формате YYYY-MM-DD HH:MM:SS
dt = datetime.datetime.strptime(install_time, "%Y-%m-%d %H:%M:%S")
install_time = dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
try:
# Пробуем разобрать ISO 8601 (если в CSV другой формат)
dt = datetime.datetime.strptime(install_time, "%Y-%m-%dT%H:%M:%S.%fZ")
install_time = dt.strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(status_code=400, detail=f"Неверный формат времени для записи с ID {rust_id}. Используйте YYYY-MM-DD HH:MM:SS или ISO 8601")
# Получаем или создаем ID папки
if folder_name:
cursor.execute("SELECT id FROM folders WHERE name = ?", (folder_name,))
folder = cursor.fetchone()
folder_id = folder[0] if folder else unsorted_folder_id
else:
folder_id = target_folder_id
cursor.execute("""
INSERT INTO installs (rust_id, computer_name, install_time, folder_id, protocol, note)
VALUES (?, ?, ?, ?, ?, ?)
""", (rust_id, computer_name, install_time, folder_id, protocol, note))
conn.commit()
return {"status": "success", "message": "Данные успешно импортированы"}
except Exception as e:
raise HTTPException(status_code=400, detail=f"Ошибка импорта: {str(e)}")
# CORS
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

@ -1,9 +1,13 @@
# /opt/rustdesk-organizer/docker-compose.yml
version: '3.8' version: '3.8'
services: services:
rustdesk-organizer: rustdesk-organizer:
build: . build: .
ports: ports:
- "8001:8001" - "8001:8001" # Порт для GET-запросов (веб-интерфейс)
- "8002:8002" # Порт для POST-запросов (API)
volumes: volumes:
- ./db:/db - ./db:/db
restart: unless-stopped restart: unless-stopped