diff --git a/compilers/base.py b/compilers/base.py new file mode 100644 index 0000000..eb991d7 --- /dev/null +++ b/compilers/base.py @@ -0,0 +1,116 @@ +import asyncio, tempfile, os, shutil +import subprocess, uuid + +from typing import Dict, Any, Optional + + +class CompilerBase: + def __init__(self, language: str, timeout: int = 5): + self.language = language + self.timeout = timeout + self.max_output_size = 1000000 # 1MB + + async def compile_and_run(self, code: str, input_data: str = "") -> Dict[str, Any]: + temp_dir = None + try: + temp_dir = tempfile.mkdtemp(prefix="compile_") + result = await self._execute(code, input_data, temp_dir) + return result + except Exception as e: + return { + "success": False, + "error": f"Compilation error: {str(e)}" + } + finally: + if temp_dir and os.path.exists(temp_dir): + try: + shutil.rmtree(temp_dir, ignore_errors=True) + except: + pass + + async def _execute(self, code: str, input_data: str, temp_dir: str) -> Dict[str, Any]: + raise NotImplementedError("Subclass must implement _execute method") + + async def _run_process(self, cmd: list, input_data: str = "", + cwd: Optional[str] = None) -> Dict[str, Any]: + try: + process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=cwd + ) + + stdout, stderr = await asyncio.wait_for( + process.communicate(input=input_data.encode() if input_data else None), + timeout=self.timeout + ) + + stdout_str = stdout.decode('utf-8', errors='replace')[:self.max_output_size] + stderr_str = stderr.decode('utf-8', errors='replace')[:self.max_output_size] + + if process.returncode != 0: + return { + "success": False, + "error": stderr_str or "Execution failed", + "output": stdout_str + } + + return { + "success": True, + "output": stdout_str, + "error": stderr_str if stderr_str else None + } + + except asyncio.TimeoutError: + if 'process' in locals(): + process.kill() + return { + "success": False, + "error": f"Execution timeout ({self.timeout}s exceeded)" + } + except Exception as e: + return { + "success": False, + "error": f"Execution error: {str(e)}" + } + + def _sanitize_code(self, code: str) -> str: + # Базовая очистка кода + dangerous_patterns = [ + "system(", "exec(", "eval(", "__import__", + "subprocess", "os.system", "popen" + ] + + for pattern in dangerous_patterns: + if pattern in code: + return "" + + return code + + def _check_input_requirements(self, code: str) -> Dict[str, Any]: + # Проверка требований к вводу + input_keywords = { + "cpp": ["cin", "scanf", "getline"], + "python": ["input(", "raw_input"], + "javascript": ["readline", "prompt"] + } + + keywords = input_keywords.get(self.language, []) + for keyword in keywords: + if keyword in code: + return { + "requiresInput": True, + "inputDescription": { + "variables": [ + { + "name": "input", + "type": "string", + "description": "Program input data" + } + ] + } + } + + return {"requiresInput": False} \ No newline at end of file diff --git a/compilers/cpp.py b/compilers/cpp.py new file mode 100644 index 0000000..89a87ac --- /dev/null +++ b/compilers/cpp.py @@ -0,0 +1,72 @@ +import os +from compilers.base import CompilerBase + +class CppCompiler(CompilerBase): + def __init__(self): + super().__init__("cpp", timeout=5) + + async def _execute(self, code: str, input_data: str, temp_dir: str): + code = self._sanitize_code(code) + if not code: + return { + "success": False, + "error": "Code contains forbidden operations" + } + + source_file = os.path.join(temp_dir, "main.cpp") + exe_file = os.path.join(temp_dir, "main") + + # Записываем код + with open(source_file, 'w') as f: + f.write(code) + + # Компиляция + compile_result = await self._run_process( + ["g++", "-o", exe_file, source_file, "-std=c++17", "-O2", "-Wall"], + cwd=temp_dir + ) + + if not compile_result["success"]: + # Парсинг ошибок компиляции + error_lines = compile_result["error"].split('\n') + for line in error_lines: + if "error:" in line: + parts = line.split(':') + if len(parts) >= 3: + try: + line_num = int(parts[1]) + message = ':'.join(parts[3:]).strip() + return { + "success": False, + "error": "Compilation error", + "details": { + "line": line_num, + "message": message + } + } + except: + pass + + return { + "success": False, + "error": compile_result["error"] + } + + # Проверка на требования ввода + input_check = self._check_input_requirements(code) + if input_check["requiresInput"] and not input_data: + return { + "success": False, + "requiresInput": True, + "inputDescription": input_check["inputDescription"], + "error": "Program requires input" + } + + # Запуск + run_result = await self._run_process( + [exe_file], + input_data=input_data, + cwd=temp_dir + ) + + return run_result \ No newline at end of file diff --git a/compilers/javascript.py b/compilers/javascript.py new file mode 100644 index 0000000..1205673 --- /dev/null +++ b/compilers/javascript.py @@ -0,0 +1,88 @@ +import os +from compilers.base import CompilerBase + +class JavaScriptCompiler(CompilerBase): + def __init__(self): + super().__init__("javascript", timeout=5) + + async def _execute(self, code: str, input_data: str, temp_dir: str): + code = self._sanitize_code(code) + if not code: + return { + "success": False, + "error": "Code contains forbidden operations" + } + + source_file = os.path.join(temp_dir, "main.js") + + # Обёртка для обработки ввода в Node.js + if input_data: + wrapped_code = f""" + const readline = require('readline'); + const rl = readline.createInterface({{ + input: process.stdin, + output: process.stdout + }}); + + let inputLines = """ + repr(input_data.strip().split("\n")) + """; + let currentLine = 0; + + global.readLine = function() {{ + if (currentLine < inputLines.length) {{ + return inputLines[currentLine++]; + }} + return ''; + }}; + + {code} + + rl.close(); + """ + else: + wrapped_code = code + + # Записываем код + with open(source_file, 'w') as f: + f.write(wrapped_code) + + # Проверка на требования ввода + input_check = self._check_input_requirements(code) + if input_check["requiresInput"] and not input_data: + return { + "success": False, + "requiresInput": True, + "inputDescription": input_check["inputDescription"], + "error": "Program requires input" + } + + # Запуск Node.js + run_result = await self._run_process( + ["node", source_file], + cwd=temp_dir + ) + + # Парсинг ошибок JavaScript + if not run_result["success"] and run_result.get("error"): + error_lines = run_result["error"].split('\n') + for line in error_lines: + if ".js:" in line: + try: + parts = line.split(':') + if len(parts) >= 2: + line_num = int(parts[1]) + # Поиск сообщения об ошибке + for err_line in error_lines: + if "Error:" in err_line or "error:" in err_line: + message = err_line.strip() + return { + "success": False, + "error": "Runtime error", + "details": { + "line": line_num, + "message": message + } + } + except: + pass + + return run_result \ No newline at end of file diff --git a/compilers/python.py b/compilers/python.py new file mode 100644 index 0000000..1d0a6ad --- /dev/null +++ b/compilers/python.py @@ -0,0 +1,67 @@ +import os +from compilers.base import CompilerBase + +class PythonCompiler(CompilerBase): + def __init__(self): + super().__init__("python", timeout=5) + + async def _execute(self, code: str, input_data: str, temp_dir: str): + code = self._sanitize_code(code) + if not code: + return { + "success": False, + "error": "Code contains forbidden operations" + } + + source_file = os.path.join(temp_dir, "main.py") + + # Записываем код + with open(source_file, 'w') as f: + f.write(code) + + # Проверка на требования ввода + input_check = self._check_input_requirements(code) + if input_check["requiresInput"] and not input_data: + return { + "success": False, + "requiresInput": True, + "inputDescription": input_check["inputDescription"], + "error": "Program requires input" + } + + # Запуск Python + run_result = await self._run_process( + ["python3", source_file], + input_data=input_data, + cwd=temp_dir + ) + + # Парсинг ошибок Python + if not run_result["success"] and run_result.get("error"): + error_lines = run_result["error"].split('\n') + for i, line in enumerate(error_lines): + if "line" in line: + try: + # Извлечение номера строки + parts = line.split(',') + for part in parts: + if "line" in part: + line_num = int(part.split()[-1]) + # Поиск сообщения об ошибке + if i + 1 < len(error_lines): + for j in range(i, len(error_lines)): + if "Error:" in error_lines[j]: + message = error_lines[j].strip() + return { + "success": False, + "error": "Runtime error", + "details": { + "line": line_num, + "message": message + } + } + break + except: + pass + + return run_result \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..2f90545 --- /dev/null +++ b/database.py @@ -0,0 +1,168 @@ +import sqlite3, json +from typing import Optional, List, Dict, Any +from datetime import datetime + + +class DBase: + def __init__(self, db_path): + """Подключаемся к БД и сохраняем курсор соединения""" + self.connection = sqlite3.connect(db_path) + self.connection.row_factory = sqlite3.Row + self.cursor = self.connection.cursor() + + def init_db(self): + with self.connection: + # Таблица пользователей + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email VARCHAR(100) UNIQUE NOT NULL, + username VARCHAR(20) UNIQUE NOT NULL, + password VARCHAR(64) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Таблица файлов и папок + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name VARCHAR(100) NOT NULL, + type VARCHAR(10) NOT NULL, + size VARCHAR(20), + folder_id INTEGER, + code TEXT, + code_lang VARCHAR(20), + modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (folder_id) REFERENCES files(id) ON DELETE CASCADE + ) + ''') + + # Таблица лимитов + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS limits ( + user_id INTEGER PRIMARY KEY, + count_files INTEGER DEFAULT 0, + length_code INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) + ''') + + # Пользователи + def create_user(self, email: str, username: str, password: str) -> int: + with self.connection: + self.cursor.execute( + "INSERT INTO users (email, username, password) VALUES (?, ?, ?)", + (email, username, password) + ) + user_id = self.cursor.lastrowid + + # Создаём запись лимитов + self.cursor.execute( + "INSERT INTO limits (user_id, count_files, length_code) VALUES (?, 0, 0)", + (user_id,) + ) + return user_id + + def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: + with self.connection: + self.cursor.execute("SELECT * FROM users WHERE username = ?", (username,)) + row = self.cursor.fetchone() + return dict(row) if row else None + + def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: + with self.connection: + self.cursor.execute("SELECT * FROM users WHERE email = ?", (email,)) + row = self.cursor.fetchone() + return dict(row) if row else None + + # Файлы + def create_file(self, user_id: int, name: str, file_type: str, size: str, + folder_id: Optional[int] = None, code: Optional[str] = None, + code_lang: Optional[str] = None) -> int: + with self.connection: + self.cursor.execute(''' + INSERT INTO files (user_id, name, type, size, folder_id, code, code_lang, modified) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', (user_id, name, file_type, size, folder_id, code, code_lang, + datetime.now().isoformat())) + + file_id = self.cursor.lastrowid + + # Обновляем лимиты + if file_type == "file": + self.cursor.execute( + "UPDATE limits SET count_files = count_files + 1 WHERE user_id = ?", + (user_id,) + ) + if code: + self.cursor.execute( + "UPDATE limits SET length_code = length_code + ? WHERE user_id = ?", + (len(code), user_id) + ) + + return file_id + + def get_user_files(self, user_id: int) -> List[Dict[str, Any]]: + with self.connection: + self.cursor.execute( + "SELECT * FROM files WHERE user_id = ? ORDER BY type DESC, name", + (user_id,) + ) + rows = self.cursor.fetchall() + return [dict(row) for row in rows] + + def get_file_by_id(self, file_id: int) -> Optional[Dict[str, Any]]: + with self.connection: + self.cursor.execute("SELECT * FROM files WHERE id = ?", (file_id,)) + row = self.cursor.fetchone() + return dict(row) if row else None + + def update_file_folder(self, file_id: int, folder_id: Optional[int]): + with self.connection: + self.cursor.execute( + "UPDATE files SET folder_id = ?, modified = ? WHERE id = ?", + (folder_id, datetime.now().isoformat(), file_id) + ) + + def delete_file(self, file_id: int): + with self.connection: + # Получаем информацию о файле + self.cursor.execute("SELECT * FROM files WHERE id = ?", (file_id,)) + file = self.cursor.fetchone() + + if file: + file_dict = dict(file) + user_id = file_dict["user_id"] + + # Если это файл, обновляем лимиты + if file_dict["type"] == "file": + self.cursor.execute( + "UPDATE limits SET count_files = count_files - 1 WHERE user_id = ?", + (user_id,) + ) + if file_dict["code"]: + self.cursor.execute( + "UPDATE limits SET length_code = length_code - ? WHERE user_id = ?", + (len(file_dict["code"]), user_id) + ) + + # Удаляем файл + self.cursor.execute("DELETE FROM files WHERE id = ?", (file_id,)) + + # Если это папка, удаляем все файлы в ней + if file_dict["type"] == "folder": + self.cursor.execute("DELETE FROM files WHERE folder_id = ?", (file_id,)) + + def get_user_limits(self, user_id: int) -> Dict[str, int]: + with self.connection: + self.cursor.execute("SELECT * FROM limits WHERE user_id = ?", (user_id,)) + row = self.cursor.fetchone() + return dict(row) if row else {"count_files": 0, "length_code": 0} + + # ЗАКРЫТИЕ ВЫЗОВА + def close(self): + """Закрываем соединение с БД""" + self.connection.close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4543b3c --- /dev/null +++ b/main.py @@ -0,0 +1,319 @@ +from fastapi import FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware + +from pydantic import BaseModel +from typing import Optional, List +from collections import defaultdict, deque +import asyncio, time, hashlib + +from database import DBase +from compilers.cpp import CppCompiler +from compilers.python import PythonCompiler +from compilers.javascript import JavaScriptCompiler + +app = FastAPI() +db = DBase("compilehub.db") + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Компиляторы +compilers = { + "cpp": CppCompiler(), + "python": PythonCompiler(), + "javascript": JavaScriptCompiler(), +} + +# Очередь задач компиляции +compilation_queue = asyncio.Queue(maxsize=100) +compilation_results = {} + +# Rate limiting +rate_limit_storage = defaultdict(lambda: {"count": 0, "reset_time": time.time() + 60}) +MAX_REQUESTS_PER_MINUTE = 30 + +# Метрики +metrics = { + "total_compilations": 0, + "failed_compilations": 0, + "avg_compilation_time": 0, + "compilation_times": deque(maxlen=100), + "active_users": set(), +} + +# Модели Pydantic +class UserRegister(BaseModel): + email: str + username: str + password: str + +class UserLogin(BaseModel): + username: str + password: str + +class FileItem(BaseModel): + id: int + name: str + type: str # "file" или "folder" + size: str + modified: str + folder: Optional[int] = None + code: Optional[str] = None + code_lang: Optional[str] = None + +class MoveFileRequest(BaseModel): + folderId: Optional[int] + +class MigrateFilesRequest(BaseModel): + userId: str + files: List[FileItem] + +# Rate limiting +def check_rate_limit(client_ip: str): + current_time = time.time() + limits = rate_limit_storage[client_ip] + + if current_time >= limits["reset_time"]: + limits["count"] = 0 + limits["reset_time"] = current_time + 60 + + if limits["count"] >= MAX_REQUESTS_PER_MINUTE: + return False + + limits["count"] += 1 + return True + +# Worker для обработки компиляций +async def compilation_worker(): + while True: + try: + task = await compilation_queue.get() + task_id = task["id"] + code = task["code"] + language = task["language"] + input_data = task.get("input", "") + + start_time = time.time() + + compiler = compilers.get(language, compilers["cpp"]) + result = await compiler.compile_and_run(code, input_data) + + execution_time = time.time() - start_time + + # Обновление метрик + metrics["total_compilations"] += 1 + if not result["success"]: + metrics["failed_compilations"] += 1 + metrics["compilation_times"].append(execution_time) + metrics["avg_compilation_time"] = sum(metrics["compilation_times"]) / len(metrics["compilation_times"]) + + result["executionTime"] = execution_time + compilation_results[task_id] = result + + except Exception as e: + print(f"Worker error: {e}") + compilation_results[task_id] = { + "success": False, + "error": str(e) + } + +@app.on_event("startup") +async def startup_event(): + db.init_db() + asyncio.create_task(compilation_worker()) + +# Auth endpoints +@app.post("/api/auth/register") +async def register(user: UserRegister): + existing = db.get_user_by_username(user.username) + if existing: + raise HTTPException(400, "Username already taken") + + existing_email = db.get_user_by_email(user.email) + if existing_email: + raise HTTPException(400, "Email already registered") + + password_hash = hashlib.sha256(user.password.encode()).hexdigest() + user_id = db.create_user(user.email, user.username, password_hash) + + return {"message": "Successfully signed up, please login", "success": True} + +@app.post("/api/auth/login") +async def login(user: UserLogin): + db_user = db.get_user_by_username(user.username) + if not db_user: + raise HTTPException(401, "Invalid credentials") + + password_hash = hashlib.sha256(user.password.encode()).hexdigest() + if db_user["password"] != password_hash: + raise HTTPException(401, "Invalid credentials") + + metrics["active_users"].add(db_user["id"]) + + return { + "id": str(db_user["id"]), + "email": db_user["email"], + "username": db_user["username"], + "token": hashlib.sha256(f"{db_user['id']}{time.time()}".encode()).hexdigest(), + "isGuest": False + } + +@app.post("/api/auth/logout") +async def logout(): + return {"message": "Logged out successfully"} + +@app.get("/api/files") +async def get_files(userId: str): + user_files = db.get_user_files(int(userId)) + items = [] + + for file in user_files: + item = { + "id": file["id"], + "name": file["name"], + "type": file["type"], + "size": file["size"], + "modified": file["modified"], + } + if file["type"] == "file": + item["folder"] = file["folder_id"] + items.append(item) + + return items + +@app.delete("/api/files/{file_id}") +async def delete_file(file_id: int): + db.delete_file(file_id) + return {"message": "File deleted"} + +@app.patch("/api/files/{file_id}/move") +async def move_file(file_id: int, request: MoveFileRequest): + db.update_file_folder(file_id, request.folderId) + return {"message": "File moved"} + +@app.post("/api/files/migrate") +async def migrate_files(request: MigrateFilesRequest): + for file in request.files: + db.create_file( + user_id=int(request.userId), + name=file.name, + file_type=file.type, + size=file.size, + folder_id=file.folder, + code=file.code, + code_lang=file.code_lang + ) + return {"message": "Files migrated"} + +# Compilation endpoints +@app.get("/api/code") +async def get_code(fileId: int): + file = db.get_file_by_id(fileId) + if not file: + raise HTTPException(404, "File not found") + return file.get("code", "") + +class CompileRequest(BaseModel): + code: str + language: Optional[str] = None + input: Optional[str] = None + +@app.post("/api/compile/") +async def compile_code_post(request: Request, compile_req: CompileRequest): + client_ip = request.client.host + + if not check_rate_limit(client_ip): + raise HTTPException(429, "Rate limit exceeded") + + code = compile_req.code + input_data = compile_req.input or "" + + # Определение языка + language = compile_req.language + if not language: + if "#include" in code: + language = "cpp" + elif "print(" in code or "def " in code or "import " in code: + language = "python" + elif "console.log" in code or "function" in code or "const " in code: + language = "javascript" + else: + language = "cpp" + + task_id = hashlib.sha256(f"{code}{time.time()}".encode()).hexdigest()[:16] + + await compilation_queue.put({ + "id": task_id, + "code": code, + "language": language, + "input": input_data + }) + + # Ждём результат (max 10 секунд) + for _ in range(100): + if task_id in compilation_results: + result = compilation_results.pop(task_id) + return result + await asyncio.sleep(0.1) + + return { + "success": False, + "error": "Compilation timeout" + } + +@app.get("/api/compile/") +async def compile_code(request: Request, code: str, input: Optional[str] = None): + client_ip = request.client.host + + if not check_rate_limit(client_ip): + raise HTTPException(429, "Rate limit exceeded") + + # Определение языка по расширению или синтаксису + language = "cpp" # по умолчанию + if "print(" in code or "def " in code or "import " in code: + language = "python" + elif "console.log" in code or "function" in code: + language = "javascript" + + task_id = hashlib.sha256(f"{code}{time.time()}".encode()).hexdigest()[:16] + + await compilation_queue.put({ + "id": task_id, + "code": code, + "language": language, + "input": input or "" + }) + + # Ждём результат (max 10 секунд) + for _ in range(100): + if task_id in compilation_results: + result = compilation_results.pop(task_id) + return result + await asyncio.sleep(0.1) + + return { + "success": False, + "error": "Compilation timeout" + } + +# Metrics endpoint +@app.get("/api/metrics") +async def get_metrics(): + return { + "total_compilations": metrics["total_compilations"], + "failed_compilations": metrics["failed_compilations"], + "success_rate": ((metrics["total_compilations"] - metrics["failed_compilations"]) / max(metrics["total_compilations"], 1)) * 100, + "avg_compilation_time": round(metrics["avg_compilation_time"], 3), + "active_users": len(metrics["active_users"]), + "queue_size": compilation_queue.qsize() + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="192.168.3.29", port=9999) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b923092 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +python-multipart==0.0.6 +aiofiles==23.2.1 \ No newline at end of file