commit 3f1c69a723faeca3881be911e7fe0ccdf9a76a96 Author: naeel Date: Mon Aug 17 17:05:20 2026 +0400 LoadTest: проверка загрузки файлов (Flask, site/app.py) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fbccab5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Зависимости LoadTest-сервиса (платформа Nubes читает этот файл из корня репо). +Flask>=3.0 +gunicorn>=21.2 +requests>=2.31 diff --git a/site/__pycache__/app.cpython-312.pyc b/site/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..5c30106 Binary files /dev/null and b/site/__pycache__/app.cpython-312.pyc differ diff --git a/site/app.py b/site/app.py new file mode 100644 index 0000000..ccb64ec --- /dev/null +++ b/site/app.py @@ -0,0 +1,92 @@ +""" +LoadTest - сервис проверки загрузки файлов на платформу Nubes (Flask). + +ЦЕЛЬ: проверить, доходят ли файлы (в т.ч. большие) до бэкенда или нет. +Файл принимается в ПАМЯТЬ (без записи на диск) и возвращается МАКСИМУМ статистики. + +Обязательная структура для платформы Nubes: + repo/ + ├── requirements.txt + └── site/ + ├── app.py <- этот файл (точка входа: python site/app.py) + ├── static/style.css + └── templates/index.html +""" +import hashlib +import time + +from flask import Flask, jsonify, render_template, request + +# Явно указываем папки шаблонов и статики - +# платформа запускает `python site/app.py` из корня репо, иначе Flask не найдёт шаблоны. +app = Flask(__name__, template_folder="templates", static_folder="static") + +# ВАЖНО: НЕ ставим MAX_CONTENT_LENGTH (по умолчанию лимита нет). +# Цель - проверить РЕАЛЬНЫЕ лимиты платформы/прокси, а не собственный лимит Flask. + + +@app.route("/") +def index(): + """Главная страница - форма выбора файла и загрузки.""" + return render_template("index.html") + + +@app.route("/health") +def health(): + """Проверка живости для платформы.""" + return "ok" + + +@app.route("/upload", methods=["POST"]) +def upload(): + """Приём файла в память + сбор максимальной статистики.""" + t0 = time.time() # момент начала обработки запроса на сервере + + # Метаданные запроса (заголовки, адрес клиента) - для диагностики. + meta = { + "server_start": time.strftime("%Y-%m-%d %H:%M:%S"), + "remote_addr": request.remote_addr or "", + "content_length_header": request.headers.get("Content-Length", ""), + "content_type_header": request.headers.get("Content-Type", ""), + "user_agent": request.headers.get("User-Agent", ""), + } + + # Валидация: должен прийти multipart-файл в поле "file". + if "file" not in request.files: + return jsonify({"ok": False, "error": "Нет поля 'file' в запросе", "meta": meta}), 400 + + f = request.files["file"] + if not f or f.filename == "": + return jsonify({"ok": False, "error": "Пустое имя файла", "meta": meta}), 400 + + # Читаем ВЕСЬ файл в память и замеряем время чтения. + t_read = time.time() + data = f.read() + read_s = time.time() - t_read + + size = len(data) # сколько байт РЕАЛЬНО дошло до сервера + total_s = time.time() - t0 # полное время обработки запроса на сервере + + result = { + "ok": True, + "file": { + "name": f.filename, + "mime": f.mimetype or "", + "size_bytes": size, + "size_mb": round(size / (1024 * 1024), 3), + }, + "server": { + "read_ms": round(read_s * 1000, 2), # время чтения в память + "total_ms": round(total_s * 1000, 2), # полное время обработки + "speed_mb_s": round(size / (1024 * 1024) / read_s, 3) if read_s > 0 else 0, + }, + "md5": hashlib.md5(data).hexdigest(), # контрольная сумма принятых байт + "meta": meta, + } + return jsonify(result) + + +if __name__ == "__main__": + # ОБЯЗАТЕЛЬНО для платформы: `python site/app.py` должен стартовать сервер. + # debug=False - прод-режим, без reloader (иначе поднимается второй процесс). + app.run(host="0.0.0.0", port=5000, debug=False) diff --git a/site/static/style.css b/site/static/style.css new file mode 100644 index 0000000..99aad2b --- /dev/null +++ b/site/static/style.css @@ -0,0 +1,128 @@ +/* LoadTest - простые стили страницы проверки загрузки файлов. */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Arial, Helvetica, sans-serif; + background: #f4f6f8; + color: #222; +} + +.wrap { + max-width: 860px; + margin: 40px auto; + padding: 0 16px; +} + +h1 { + font-size: 22px; + margin-bottom: 6px; +} + +.hint { + color: #555; + font-size: 14px; + margin-top: 0; +} + +/* Панель выбора файла */ +.panel { + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + padding: 16px; + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.panel label { + font-weight: bold; +} + +.panel input[type="file"] { + flex: 1; + min-width: 200px; +} + +.panel button { + background: #2563eb; + color: #fff; + border: none; + border-radius: 6px; + padding: 10px 20px; + font-size: 14px; + cursor: pointer; +} + +.panel button:hover { + background: #1d4ed8; +} + +/* Прогресс загрузки */ +.progress-wrap { + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + padding: 16px; + margin-top: 16px; +} + +.progress-bar { + height: 18px; + background: #e5e7eb; + border-radius: 9px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + width: 0%; + background: #22c55e; + transition: width 0.2s; +} + +.progress-info { + display: flex; + gap: 16px; + margin-top: 8px; + font-size: 14px; + font-weight: bold; +} + +/* Итоговый статус */ +.status { + margin-top: 16px; + padding: 14px; + border-radius: 8px; + font-size: 16px; + font-weight: bold; +} + +.status.ok { + background: #dcfce7; + color: #166534; + border: 1px solid #86efac; +} + +.status.error { + background: #fee2e2; + color: #991b1b; + border: 1px solid #fca5a5; +} + +/* Блок статистики */ +#result { + background: #0f172a; + color: #e2e8f0; + padding: 16px; + border-radius: 8px; + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-all; + margin-top: 16px; +} diff --git a/site/templates/index.html b/site/templates/index.html new file mode 100644 index 0000000..0a5518c --- /dev/null +++ b/site/templates/index.html @@ -0,0 +1,211 @@ + + + + + + + LoadTest - проверка загрузки файлов + + + +
+

LoadTest - проверка загрузки файлов

+

+ Выберите ОДИН файл (в т.ч. большой) и нажмите «Загрузить». + Файл читается в память Flask (на диск не пишется). Результат: файл дошёл или нет + статистика. +

+ + +
+ + + +
+ + + + + +
+ + +

+  
+ + + +