Фаза 1: удалён Dockerfile, drhider_server.py, gunicorn из requirements, __main__ блок
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY drhider.py drhider_server.py /app/
|
||||
COPY site /app/site
|
||||
EXPOSE 5000
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "300", "--workers", "1", "--worker-class", "gevent", "--limit-request-field_size", "0", "--limit-request-body", "0", "site.app:app"]
|
||||
@@ -0,0 +1,95 @@
|
||||
# DrHider — Полный аудит и план рефакторинга
|
||||
|
||||
**Дата:** 2026-07-12
|
||||
**Платформа:** Штурвал (Managed Flask) — запускает Flask сам, без Dockerfile/gunicorn
|
||||
**URL:** https://drhider.pythonk8s.dev.nubes.ru/
|
||||
**Репозиторий:** https://gitea.services.ngcloud.ru/Nail/drhider
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (аудит каждого файла)
|
||||
|
||||
| Файл | Статус | Решение |
|
||||
|---|---|---|
|
||||
| `Dockerfile` | ❌ НЕ НУЖЕН | Штурвал сам запускает Flask. Удалить. |
|
||||
| `drhider_server.py` | ❌ НЕ НУЖЕН | Standalone-сервер с ВМ (:8767). Flask app.py уже обрабатывает /api/drhider. Удалить. |
|
||||
| `requirements.txt` | ⚠️ Нужны правки | Убрать `gunicorn` и `gevent`. Оставить `flask`, `python-docx`, `pdfplumber`, `httpx`. |
|
||||
| `.gitignore` | ✅ OK | Без изменений. |
|
||||
| `.gitea/workflows/deploy.yaml` | ✅ OK | CI для Штурвала. |
|
||||
| `site/app.py` | ⚠️ Нужны правки | Удалить блок `if __name__ == "__main__"` (gunicorn). При рефакторинге — вынести LLMClient. |
|
||||
| `site/templates/index.html` | ✅ OK | UI. |
|
||||
| `site/static/favicon.svg` | ✅ OK | Иконка (из loadtest). |
|
||||
| `drhider.py` | ⚠️ МОНОЛИТ | 560 строк. Разбить на ~20 модулей. |
|
||||
| `History/` | ✅ OK | Документация. |
|
||||
|
||||
---
|
||||
|
||||
## План рефакторинга (Фаза 1: удаление лишнего)
|
||||
|
||||
### Шаг 1.1 — Удалить `Dockerfile`
|
||||
### Шаг 1.2 — Удалить `drhider_server.py`
|
||||
### Шаг 1.3 — Исправить `requirements.txt` (убрать gunicorn, gevent)
|
||||
### Шаг 1.4 — Исправить `site/app.py` (убрать блок `if __name__ == "__main__"`)
|
||||
### Шаг 1.5 — Проверить синтаксис → commit → push
|
||||
|
||||
---
|
||||
|
||||
## План рефакторинга (Фаза 2: модульная структура)
|
||||
|
||||
### Новая структура `drhider/` пакета (вместо `drhider.py` 560 строк):
|
||||
|
||||
```
|
||||
drhider/ # Пакет ядра обфускации
|
||||
├── __init__.py # re-export: obfuscate_files()
|
||||
├── config.py # Константы: ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN,
|
||||
│ # RU_SURNAMES, RU_NAMES, RU_PATRONYMICS,
|
||||
│ # RU_CITIES, RU_STREETS, FAKE_DOMAINS
|
||||
├── checksum.py # _checksum_inn10, _checksum_inn12, _checksum_ogrn
|
||||
├── random_utils.py # _random_digits, _random_letters
|
||||
├── generators/ # Генераторы фиктивных значений
|
||||
│ ├── __init__.py # ENTITY_GENERATORS маппинг
|
||||
│ ├── phone.py # generate_phone
|
||||
│ ├── email.py # generate_email
|
||||
│ ├── inn.py # generate_inn10, generate_inn12
|
||||
│ ├── ogrn.py # generate_ogrn
|
||||
│ ├── kpp.py # generate_kpp
|
||||
│ ├── bik.py # generate_bik
|
||||
│ ├── accounts.py # generate_rs, generate_ks
|
||||
│ ├── passport.py # generate_passport
|
||||
│ ├── company.py # generate_company
|
||||
│ ├── person.py # generate_person
|
||||
│ └── address.py # generate_address
|
||||
├── extractor.py # _extract_text, _expand_zips, _convert_pdfs_to_docx
|
||||
├── scanner.py # Проход 1: _scan_regex, _scan_llm_ner
|
||||
├── replacer.py # Проход 2: _replace_in_docx, _replace_in_text, _apply_replacements
|
||||
├── builder.py # Сборка: _build_zip, _build_mapping_csv
|
||||
├── obfuscator.py # TwoPassObfuscator — оркестратор
|
||||
└── llm_client.py # LLMClient (из app.py → сюда)
|
||||
|
||||
site/ # Flask-приложение
|
||||
├── __init__.py
|
||||
├── app.py # Только create_app() + регистрация blueprint'ов
|
||||
├── routes/
|
||||
│ ├── __init__.py # Регистрация всех blueprint'ов
|
||||
│ ├── main_bp.py # GET /
|
||||
│ ├── health_bp.py # GET /health
|
||||
│ └── api_bp.py # POST /api/drhider
|
||||
├── templates/index.html
|
||||
└── static/favicon.svg
|
||||
```
|
||||
|
||||
### Принципы:
|
||||
- Каждый файл ≤ 50 строк (где возможно)
|
||||
- Каждый файл — одна ответственность
|
||||
- Максимум docstring и инлайн-комментариев
|
||||
- Все импорты явные, никаких `import *`
|
||||
|
||||
---
|
||||
|
||||
## Переменные окружения (для Штурвала)
|
||||
|
||||
| Переменная | Значение |
|
||||
|---|---|
|
||||
| `LLM_API_KEY` | `sk-ucI5YvOticoOQ9Kuj5K9mQ` |
|
||||
| `LLM_URL` | `https://api.aillm.ru/v1/chat/completions` |
|
||||
| `LLM_MODEL` | `gpt-oss-120b` |
|
||||
@@ -0,0 +1,4 @@
|
||||
НЕ СПЕШИ ! не ошибайся. НЕ ПРЕДПОЛАГАЙ !
|
||||
не надо ДОГАДОК !
|
||||
не надо самостоятельно что либо "УЛуЧШАТЬ" - никаких изменений рабочего кода без прямого разрешения
|
||||
есть сомнения - лучше остановись и спроси
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DrHider standalone server — NO DB dependency. Port 8767."""
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn, TCPServer
|
||||
from urllib.parse import urlparse
|
||||
import json, re, os, sys, cgi
|
||||
|
||||
DRHIDER_SERVER_VERSION = "1.3"
|
||||
|
||||
MAX_BODY = 200 * 1024 * 1024 # 200 MB
|
||||
|
||||
# ── Загрузка .env ────────────────────────────────────────────────────────
|
||||
_env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".env")
|
||||
if os.path.exists(_env_path):
|
||||
with open(_env_path) as _f:
|
||||
for _line in _f:
|
||||
_line = _line.strip()
|
||||
if _line and not _line.startswith("#") and "=" in _line:
|
||||
_k, _v = _line.split("=", 1)
|
||||
if _k not in os.environ:
|
||||
os.environ[_k] = _v
|
||||
|
||||
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self._send_cors()
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/health":
|
||||
self._json({"ok": True, "server": "drhider", "version": DRHIDER_SERVER_VERSION})
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/api/drhider":
|
||||
self._handle_drhider()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
# ── DrHider ──────────────────────────────────────────────────────────
|
||||
|
||||
def _handle_drhider(self):
|
||||
"""Обфускация: multipart files → ZIP."""
|
||||
from drhider import obfuscate_files
|
||||
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
if length <= 0 or length > MAX_BODY:
|
||||
self._json({"ok": False, "error": "Файл слишком большой"}, 413)
|
||||
return
|
||||
raw = self.rfile.read(length)
|
||||
ctype = self.headers.get("Content-Type", "")
|
||||
|
||||
_, params = cgi.parse_header(ctype)
|
||||
boundary = params.get("boundary", "")
|
||||
if not boundary:
|
||||
self._json({"ok": False, "error": "No boundary"}, 400)
|
||||
return
|
||||
|
||||
parts = raw.split(b"--" + boundary.encode())
|
||||
files = []
|
||||
for part in parts:
|
||||
if b"Content-Disposition" not in part:
|
||||
continue
|
||||
try:
|
||||
hdr_end = part.index(b"\r\n\r\n")
|
||||
except ValueError:
|
||||
continue
|
||||
hdrs = part[:hdr_end].decode("latin-1", errors="replace")
|
||||
body = part[hdr_end + 4:]
|
||||
if body.endswith(b"\r\n"):
|
||||
body = body[:-2]
|
||||
m = re.search(r'filename="([^"]*)"', hdrs)
|
||||
if not m:
|
||||
continue
|
||||
name = os.path.basename(m.group(1))
|
||||
try:
|
||||
name = name.encode('latin-1').decode('utf-8')
|
||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||
pass
|
||||
if not name or ".." in name or "/" in name or "\\" in name:
|
||||
continue
|
||||
files.append((name, body, ""))
|
||||
|
||||
if not files:
|
||||
self._json({"ok": False, "error": "Нет файлов"}, 400)
|
||||
return
|
||||
|
||||
class _VMLLM:
|
||||
def complete(self, prompt):
|
||||
import httpx
|
||||
key = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "")
|
||||
r = httpx.post(
|
||||
os.environ.get("LLM_URL", "https://api.aillm.ru/v1/chat/completions"),
|
||||
json={"model": os.environ.get("LLM_MODEL", "gpt-oss-120b"),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 8000, "temperature": 0.1},
|
||||
headers={"Authorization": f"Bearer {key}",
|
||||
"Content-Type": "application/json"},
|
||||
timeout=120
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()["choices"][0]["message"]["content"]
|
||||
|
||||
try:
|
||||
zip_data, _csv = obfuscate_files(files, llm_client=_VMLLM())
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/zip")
|
||||
self.send_header("Content-Disposition", 'attachment; filename="drhider_output.zip"')
|
||||
self.send_header("Content-Length", str(len(zip_data)))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(zip_data)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self._json({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
def _json(self, data, status=200):
|
||||
self.send_response(status)
|
||||
self._send_cors()
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
|
||||
|
||||
def _send_cors(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TCPServer.allow_reuse_address = True
|
||||
port = int(os.environ.get("PORT", "8767"))
|
||||
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
|
||||
print(f"DrHider server on :{port} (NO DB)")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
server.shutdown()
|
||||
@@ -1,6 +1,4 @@
|
||||
flask
|
||||
gunicorn
|
||||
gevent
|
||||
python-docx
|
||||
pdfplumber
|
||||
httpx
|
||||
|
||||
+1
-10
@@ -84,13 +84,4 @@ def api_drhider():
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
subprocess.run([
|
||||
sys.executable, "-m", "gunicorn", "app:app",
|
||||
"--bind", "0.0.0.0:5000",
|
||||
"--worker-class", "gevent",
|
||||
"--workers", "1",
|
||||
"--worker-connections", "1000",
|
||||
"--timeout", "300",
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user