From ac5eb45787e912b99ea7c9a892d2c9e07b02cb2f Mon Sep 17 00:00:00 2001 From: "Satur@it-depot.ru" Date: Wed, 5 Mar 2025 16:28:54 +0300 Subject: [PATCH] post --- Dockerfile | 12 +- app.py | 292 +++++++++++---------------------------------- docker-compose.yml | 6 +- 3 files changed, 84 insertions(+), 226 deletions(-) diff --git a/Dockerfile b/Dockerfile index a0cc6eb..e021481 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,17 @@ +# /opt/rustdesk-organizer/Dockerfile + FROM python:3.11-slim WORKDIR /app + COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt + COPY app.py . COPY templates/ ./templates/ -EXPOSE 8001 -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8001"] \ No newline at end of file + +# Экспонируем оба порта +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"] \ No newline at end of file diff --git a/app.py b/app.py index 95fd0f1..a35fec4 100644 --- a/app.py +++ b/app.py @@ -1,30 +1,23 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile, Query +from fastapi import FastAPI, HTTPException from pydantic import BaseModel import sqlite3 import datetime -import csv -import markdown from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, StreamingResponse -import io +from fastapi.responses import FileResponse +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) 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(""" CREATE TABLE IF NOT EXISTS folders ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -40,24 +33,12 @@ CREATE TABLE IF NOT EXISTS installs ( computer_name TEXT, install_time TEXT, folder_id INTEGER, - protocol TEXT DEFAULT 'rustdesk', - note TEXT DEFAULT '', FOREIGN KEY (folder_id) REFERENCES folders(id) ) """) 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): name: str parent_id: int | None = None @@ -66,246 +47,111 @@ class FolderUpdate(BaseModel): name: str class InstallData(BaseModel): - rust_id: str | None = None - computer_name: str | None = None - install_time: str | None = None # Принимаем строку в формате YYYY-MM-DD HH:MM:SS + rust_id: str + computer_name: str + install_time: str | 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") -app.mount("/icons", StaticFiles(directory="templates/icons"), name="icons") +# Функция для работы с базой данных +def get_db(): + return conn, cursor -# Главная страница -@app.get("/") +# --- GET API (порт 8001) --- +@app_get.get("/") async def root(): return FileResponse("templates/index.html") -# --- Folders API --- -@app.get("/api/folders") +@app_get.get("/api/folders") def get_folders(): + conn, cursor = get_db() cursor.execute("SELECT * FROM folders") rows = cursor.fetchall() 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): + conn, cursor = get_db() cursor.execute("INSERT INTO folders (name, parent_id) VALUES (?, ?)", (folder.name, folder.parent_id)) conn.commit() 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): + conn, cursor = get_db() cursor.execute("UPDATE folders SET name = ? WHERE id = ?", (folder.name, folder_id)) conn.commit() if cursor.rowcount == 0: raise HTTPException(status_code=404, detail="Папка не найдена") return {"status": "success"} -@app.delete("/api/folders/{folder_id}") +@app_post.delete("/api/folders/{folder_id}") def delete_folder(folder_id: int): - # Проверяем, не является ли папка "Несортированные" - 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="Папка 'Несортированные' не может быть удалена") - + conn, cursor = get_db() cursor.execute("DELETE FROM folders WHERE id = ?", (folder_id,)) conn.commit() if cursor.rowcount == 0: raise HTTPException(status_code=404, detail="Папка не найдена") return {"status": "success"} -# --- Installs API --- -@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") +@app_post.post("/api/install") def add_install(data: InstallData): - install_time = data.install_time or datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Форматируем в читаемый формат - folder_id = data.folder_id if data.folder_id is not None else unsorted_folder_id - protocol = data.protocol or 'rustdesk' # По умолчанию RustDesk для POST-запросов - note = data.note or '' # Новая заметка, по умолчанию пустая - 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)) + install_time = data.install_time or datetime.datetime.now().isoformat() + conn, cursor = get_db() + cursor.execute("INSERT INTO installs (rust_id, computer_name, install_time, folder_id) VALUES (?, ?, ?, ?)", + (data.rust_id, data.computer_name, install_time, data.folder_id)) conn.commit() return {"status": "success"} -@app.put("/api/install/{install_id}") +@app_post.put("/api/install/{install_id}") 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,)) - 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") - + conn, cursor = get_db() cursor.execute(""" UPDATE installs - SET rust_id = ?, computer_name = ?, install_time = ?, folder_id = ?, protocol = ?, note = ? + SET rust_id = ?, computer_name = ?, install_time = ?, folder_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() + if cursor.rowcount == 0: + raise HTTPException(status_code=404, detail="Запись не найдена") return {"status": "success"} -@app.delete("/api/install/{install_id}") +@app_post.delete("/api/install/{install_id}") def delete_install(install_id: int): + conn, cursor = get_db() cursor.execute("DELETE FROM installs WHERE id = ?", (install_id,)) conn.commit() if cursor.rowcount == 0: raise HTTPException(status_code=404, detail="Запись не найдена") return {"status": "success"} -# --- CSV Export/Import API --- -@app.get("/api/export/csv") -async def export_csv(folder_id: int | None = Query(None, description="ID папки для экспорта, если None - экспортировать все папки")): - if folder_id: - 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 - 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() +# CORS для обоих приложений +for app in [app_get, app_post]: + app.add_middleware( + CORSMiddleware, + allow_origins=["http://10.0.0.10:8001", "http://localhost:8001"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) - output = io.StringIO() - writer = csv.writer(output, lineterminator='\n') - writer.writerow(['ID подключения', 'Имя компьютера', 'Время установки', 'Папка', 'Протокол', 'Заметка']) - for row in rows: - # Обработка времени в разных форматах - 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=["*"], -) \ No newline at end of file +if __name__ == "__main__": + import uvicorn + # Запускаем два процесса Uvicorn для разных портов (опционально, для локального теста) + uvicorn.run(app_get, host="0.0.0.0", port=8001) + uvicorn.run(app_post, host="0.0.0.0", port=8002) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6314e60..086469a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,13 @@ +# /opt/rustdesk-organizer/docker-compose.yml + version: '3.8' + services: rustdesk-organizer: build: . ports: - - "8001:8001" + - "8001:8001" # Порт для GET-запросов (веб-интерфейс) + - "8002:8002" # Порт для POST-запросов (API) volumes: - ./db:/db restart: unless-stopped \ No newline at end of file