v0.0.58: ВМ-загрузка (паттерн ВМ-буфер+pull) — PUT на ВМ + /api/upload_refs (egress), nginx /drhider-upload/ + CORS + TTL
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
|
||||
|
||||
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
|
||||
_Статус: план, код НЕ менялся. Писать будет DeepSeek Flash._
|
||||
_Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# ВМ-загрузка в drhider — реализовано (v0.0.58, 2026-08-21)
|
||||
|
||||
_Реализация паттерна «ВМ-буфер + pull» по плану `2026-08-21-pattern-vm-buffer-pull-plan.md`._
|
||||
_Снимок до изменений: ветка `save-pre-vm-upload-2026-08-21`._
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Flask** (`site/routes/api_bp.py`) — новый `POST /api/upload_refs`:
|
||||
принимает JSON `{session, files:[{name,size,url}]}`, тянет каждый файл с ВМ
|
||||
ИСХОДЯЩИМ GET (httpx, stream по частям), кладёт в сессию `add_file()`,
|
||||
после успешного pull делает DELETE с ВМ. Ошибки egress → 502.
|
||||
2. **Фронт** (`site/templates/index.html`, `uploadFiles()`) — Фаза 1 переделана:
|
||||
- 1a: каждый файл → `PUT` на ВМ `https://contracts.kube5s.ru/drhider-upload/<токен>_<k>`
|
||||
(XHR, прогресс по-прежнему в строке), собирает refs.
|
||||
- 1b: один маленький `POST /api/upload_refs` (<64КБ).
|
||||
- Токен — `crypto.randomUUID()`. Константа `VM_UPLOAD_URL`.
|
||||
3. **ВМ** (`5.172.178.213`) — nginx `nginx-contracts.conf`:
|
||||
- `location /drhider-upload/` → `alias /var/www/drhider-upload/`,
|
||||
`dav_methods PUT DELETE`, `create_full_put_path on`, `client_max_body_size 1024m`,
|
||||
CORS-заголовки для `drhider.pythonk8s.dev.nubes.ru`, OPTIONS → 204.
|
||||
- Каталог `/var/www/drhider-upload/` (www-data).
|
||||
- Backup: `/tmp/nginx-contracts.conf.bak.20260821`.
|
||||
- TTL-чистка в root cron: `*/5 * * * * find /var/www/drhider-upload -type f -mmin +30 -delete`.
|
||||
4. **Версия** — `site/app.py`: 0.0.57 → 0.0.58.
|
||||
|
||||
## Отклонение от плана
|
||||
- URL файла на ВМ — БЕЗ имени: `.../drhider-upload/<токен>_<k>` (имя — только в метаданных).
|
||||
Иначе кириллица/пробелы в имени ломали бы URL.
|
||||
|
||||
## Тесты (2026-08-21)
|
||||
- ВМ: PUT 10МБ → 201 (1.0с); GET → 200, 10485760B; OPTIONS preflight → 204 с CORS;
|
||||
DELETE → 204; GET после DELETE → 404.
|
||||
- Сквозной: PUT на ВМ → `POST /api/upload_refs` → 200, файл в сессии (10485760B),
|
||||
файл с ВМ удалён (404).
|
||||
- `py_compile` api_bp/app — OK; `node --check` JS из index.html — OK.
|
||||
|
||||
## Замечания
|
||||
- ГРАБЛИ: на Krupski `dd` алиасится на `docker compose down` — для генерации файлов
|
||||
использовать `head -c N /dev/urandom > file`.
|
||||
- Локальные curl/httpx к contracts.kube5s.ru — через прокси (`172.17.192.1:10808`):
|
||||
для тестов `--noproxy '*'` / `NO_PROXY=contracts.kube5s.ru`.
|
||||
- Прод-безопасность (позже): токены без TTL-времени в самом URL, авторизация на ВМ
|
||||
(сейчас ключ — случайный UUID, файл живёт ≤30 мин по cron).
|
||||
Executable
+5182
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
|
||||
sys.path.insert(0, _sys_path_root)
|
||||
|
||||
# Версия приложения (меняется при изменениях)
|
||||
VERSION = "0.0.57"
|
||||
VERSION = "0.0.58"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
@@ -16,6 +16,7 @@ import threading
|
||||
import zipfile
|
||||
import traceback
|
||||
import logging
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, request, send_file, jsonify, Response, stream_with_context
|
||||
|
||||
@@ -65,6 +66,48 @@ def upload():
|
||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||
|
||||
|
||||
@api_bp.route("/upload_refs", methods=["POST"])
|
||||
def upload_refs():
|
||||
"""Принять ссылки на файлы (загружены на ВМ-буфер), забрать по egress.
|
||||
|
||||
Вход: JSON {"session": "...", "files": [{"name": str, "size": int, "url": str}]}.
|
||||
Каждый файл тянется ИСХОДЯЩИМ GET'ом с ВМ (egress не ограничен шлюзом),
|
||||
читается по частям (stream), кладётся в сессию. После успешного pull файл
|
||||
удаляется с ВМ (best-effort; TTL-чистка на ВМ тоже есть).
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = data.get("session") or create_session()
|
||||
refs = data.get("files") or []
|
||||
if not refs:
|
||||
log.warning("upload_refs: no files, sid=%s", sid)
|
||||
return jsonify({"ok": False, "error": "No files"}), 400
|
||||
added = 0
|
||||
try:
|
||||
with httpx.Client(timeout=120, follow_redirects=True) as client:
|
||||
for ref in refs:
|
||||
name = ref.get("name")
|
||||
url = ref.get("url")
|
||||
if not name or not url:
|
||||
continue
|
||||
with client.stream("GET", url) as resp:
|
||||
resp.raise_for_status()
|
||||
content = b"".join(resp.iter_bytes())
|
||||
log.info("upload_refs: pulled sid=%s file=%r size=%d", sid, name, len(content))
|
||||
if not add_file(sid, name, content):
|
||||
log.warning("upload_refs: session not found/limit, sid=%s file=%r", sid, name)
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
try:
|
||||
client.delete(url) # убрать файл с ВМ после загрузки
|
||||
except Exception:
|
||||
pass
|
||||
added += 1
|
||||
except Exception as e:
|
||||
log.error("upload_refs: pull error sid=%s: %r", sid, e)
|
||||
return jsonify({"ok": False, "error": "Pull failed: %s" % e}), 502
|
||||
log.info("upload_refs: done sid=%s added=%d total=%d", sid, added, file_count(sid))
|
||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||
|
||||
|
||||
@api_bp.route("/process_stream/<sid>", methods=["GET"])
|
||||
def process_stream(sid):
|
||||
"""SSE: process all session files, streaming per-file progress.
|
||||
|
||||
+38
-24
@@ -198,6 +198,7 @@ let fileMeta = new Map(); // имя -> {size, mtime} для дедупа/суф
|
||||
let overNames = new Set(); // имена файлов сверх лимита (не участвуют в обфускации)
|
||||
const MAX_FILE_BYTES = 100 * 1024 * 1024; // 100 МБ на файл
|
||||
const MAX_SESSION_BYTES = 1024 * 1024 * 1024; // 1 ГБ суммарно
|
||||
const VM_UPLOAD_URL = 'https://contracts.kube5s.ru/drhider-upload/'; // ВМ-буфер: PUT больших файлов (шлюз кластера их рвёт)
|
||||
let currentSid = '';
|
||||
let activeES = null; // активный EventSource
|
||||
let activeXHR = null; // активный XHR
|
||||
@@ -434,23 +435,22 @@ async function uploadFiles() {
|
||||
const total = toSend.length;
|
||||
if (total === 0) { st.className = 'status error'; st.textContent = 'Нет файлов для обфускации (все превышают лимит).'; ub.disabled = false; return; }
|
||||
|
||||
// Фаза 1: загрузка (XMLHttpRequest — прогресс в строке)
|
||||
// Фаза 1: загрузка на ВМ-буфер (PUT напрямую на nginx ВМ, минуя шлюз кластера)
|
||||
const token = crypto.randomUUID();
|
||||
const refs = []; // {name, size, url} — для POST /api/upload_refs
|
||||
for (let k = 0; k < total; k++) {
|
||||
const f = toSend[k];
|
||||
const i = sendIdx[k]; // индекс в sf для вывода прогресса
|
||||
const n = k + 1;
|
||||
st.className = 'status progress';
|
||||
st.textContent = 'Загрузка (этап 1/2) ' + n + '/' + total + ': ' + f.name;
|
||||
st.textContent = 'Загрузка на ВМ (этап 1/2) ' + n + '/' + total + ': ' + f.name;
|
||||
try {
|
||||
const t0 = performance.now();
|
||||
const vmUrl = VM_UPLOAD_URL + token + '_' + k; // имя файла в URL не несём (токен-ключ)
|
||||
await new Promise((resolve, reject) => {
|
||||
const fd = new FormData();
|
||||
fd.append('files', f, f.name);
|
||||
if (currentSid) fd.append('session', currentSid);
|
||||
const url = '/api/upload?_=' + Date.now();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url);
|
||||
xhr.timeout = 300000; // 300с: загрузка больших файлов нестабильна через шлюз
|
||||
xhr.open('PUT', vmUrl);
|
||||
xhr.timeout = 300000; // 300с: большие файлы
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const pct = Math.round(e.loaded / e.total * 100);
|
||||
@@ -458,35 +458,49 @@ async function uploadFiles() {
|
||||
}
|
||||
};
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
if (data.ok) {
|
||||
const elapsed = (performance.now() - t0) / 1000;
|
||||
const speed = f.size / elapsed;
|
||||
ss(i, '<span style="color:#22c55e">✓ ' + fs(speed) + '/s</span>');
|
||||
currentSid = data.session;
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(data.error));
|
||||
}
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
const elapsed = (performance.now() - t0) / 1000;
|
||||
const speed = f.size / elapsed;
|
||||
ss(i, '<span style="color:#22c55e">✓ ' + fs(speed) + '/s</span>');
|
||||
refs.push({ name: f.name, size: f.size, url: vmUrl });
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('HTTP ' + xhr.status));
|
||||
reject(new Error('ВМ: HTTP ' + xhr.status));
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() { reject(new Error('Сеть')); };
|
||||
xhr.ontimeout = function() { reject(new Error('Таймаут 300с')); };
|
||||
xhr.onerror = function() { reject(new Error('Сеть (ВМ)')); };
|
||||
xhr.ontimeout = function() { reject(new Error('Таймаут 300с (ВМ)')); };
|
||||
activeXHR = xhr;
|
||||
xhr.send(fd);
|
||||
xhr.send(f); // сырое тело файла
|
||||
});
|
||||
} catch (err) {
|
||||
ss(i, '<span style="color:#ef4444">✗</span>');
|
||||
st.className = 'status error';
|
||||
st.textContent = 'Ошибка загрузки: ' + err.message;
|
||||
st.textContent = 'Ошибка загрузки на ВМ: ' + err.message;
|
||||
ub.disabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Шаг 1b: один маленький POST во Flask со ссылками на файлы ВМ (<64КБ)
|
||||
st.className = 'status progress';
|
||||
st.textContent = 'Передача ссылок в сервис…';
|
||||
try {
|
||||
const resp = await fetch('/api/upload_refs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session: currentSid, files: refs })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) throw new Error(data.error || 'HTTP ' + resp.status);
|
||||
currentSid = data.session;
|
||||
} catch (err) {
|
||||
st.className = 'status error';
|
||||
st.textContent = 'Ошибка передачи ссылок: ' + err.message;
|
||||
ub.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Фаза 2: обработка (SSE — прогресс по каждому файлу)
|
||||
// Сброс загрузочных статусов — теперь этап обработки (только отправленные)
|
||||
for (let k = 0; k < total; k++) ss(sendIdx[k], '<span style="color:#2563eb">⏳</span>');
|
||||
|
||||
Reference in New Issue
Block a user