compiler c;p;j, first api and db events

This commit is contained in:
Egor Deev 2025-08-20 17:29:57 +03:00 committed by GitHub
parent 26a53da8ec
commit cae5b987d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 835 additions and 0 deletions

116
compilers/base.py Normal file
View file

@ -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}

72
compilers/cpp.py Normal file
View file

@ -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

88
compilers/javascript.py Normal file
View file

@ -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

67
compilers/python.py Normal file
View file

@ -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

168
database.py Normal file
View file

@ -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()

319
main.py Normal file
View file

@ -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)

5
requirements.txt Normal file
View file

@ -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