From 27da25c19ef468209281f24a9abd5c3fe8a8b0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 11 Jul 2026 09:48:13 +0400 Subject: [PATCH] feat: Gunicorn + HTTP POST (base64, retry), drop WebSocket, bump 1.0.19 --- site/app.py | 10 +++- site/static/app.js | 111 ++++++++++++++++++++------------------------- wstest.yaml | 72 +++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 wstest.yaml diff --git a/site/app.py b/site/app.py index de0fde6..4fa6ba2 100644 --- a/site/app.py +++ b/site/app.py @@ -7,7 +7,7 @@ from flask_sock import Sock app = Flask(__name__) sock = Sock(app) -VERSION = '1.0.18' +VERSION = '1.0.19' @app.route('/') @@ -70,4 +70,10 @@ def upload(): if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) + import subprocess, sys + subprocess.run([sys.executable, '-m', 'gunicorn', 'app:app', + '--bind', '0.0.0.0:5000', + '--workers', '2', + '--threads', '4', + '--timeout', '300', + '--keep-alive', '5']) diff --git a/site/static/app.js b/site/static/app.js index 02e0a50..00f8e00 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -155,10 +155,7 @@ async function uploadAll() { var okCount = 0; var errCount = 0; - // Один WebSocket на все файлы - var proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; - var wsUrl = proto + '//' + location.host + '/ws-upload'; - + // HTTP POST на /upload (base64, с retry) for (var i = 0; i < files.length; i++) { var f = files[i]; if (f.status === 'ok') continue; @@ -168,7 +165,7 @@ async function uploadAll() { renderTable(); try { - var result = await uploadFileWS(wsUrl, f, i); + var result = await uploadFileHTTP(f, i); f.status = 'ok'; f.elapsed = result.elapsed_ms; f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null; @@ -196,72 +193,64 @@ async function uploadAll() { fileSummary.textContent = summary; } -/** uploadFileWS(url, f, idx) — загрузить файл через WebSocket чанками */ -function uploadFileWS(url, f, idx) { +/** uploadFileHTTP(f, idx) — загрузить файл через HTTP POST (base64, с retry) */ +function uploadFileHTTP(f, idx) { return new Promise(function (resolve, reject) { - var ws = new WebSocket(url); - var CHUNK = 64 * 1024; - var offset = 0; - var startTime = Date.now(); - var lastProgress = 0; - var timer; + var reader = new FileReader(); + reader.onload = function () { + var dataUrl = reader.result; + var startTime = Date.now(); + var RETRY = 3; - ws.binaryType = 'arraybuffer'; + function doUpload(attempt) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/upload'); + xhr.timeout = 300000; - ws.onopen = function () { - var reader = new FileReader(); - reader.onload = function (e) { - ws.send(e.target.result); - offset += CHUNK; - if (offset < f.size) { - var pct = Math.round(offset / f.size * 100); - if (pct - lastProgress >= 10) { - lastProgress = pct; - f.progress = pct; + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + f.progress = Math.round(e.loaded / e.total * 100); renderTable(); } - readNext(); - } else { - ws.send('DONE'); - } - }; - function readNext() { - var end = Math.min(offset + CHUNK, f.size); - reader.readAsArrayBuffer(f.file.slice(offset, end)); + }; + + xhr.onload = function () { + try { + var r = JSON.parse(xhr.responseText); + resolve({ bytes: r.bytes, elapsed_ms: Date.now() - startTime }); + } catch (e) { + if (attempt < RETRY) { doUpload(attempt + 1); return; } + reject(new Error('Bad response')); + } + }; + + xhr.onerror = function () { + if (attempt < RETRY) { doUpload(attempt + 1); return; } + reject(new Error('Network error')); + }; + + xhr.ontimeout = function () { + if (attempt < RETRY) { doUpload(attempt + 1); return; } + reject(new Error('Timeout')); + }; + + var form = new FormData(); + form.append('data', dataUrl); + form.append('name', f.name); + form.append('orig_size', f.size); + xhr.send(form); } - readNext(); + + doUpload(1); }; - - ws.onmessage = function (e) { - try { - var r = JSON.parse(e.data); - clearTimeout(timer); - f.progress = 100; - renderTable(); - ws.close(); - resolve({ bytes: r.bytes, elapsed_ms: Date.now() - startTime }); - } catch (err) { - reject(new Error('Bad response')); - } - }; - - ws.onerror = function () { reject(new Error('WebSocket error')); }; - - ws.onclose = function () { - // Если файл не до конца — ошибка - if (offset < f.size) { - reject(new Error('WS closed early')); - } - }; - - // Таймаут на случай зависания - timer = setTimeout(function () { - ws.close(); - reject(new Error('Timeout')); - }, 300000); + reader.onerror = function () { reject(new Error('Read error')); }; + reader.readAsDataURL(f.file); }); } +// WebSocket upload больше не используется; оставлен для справки +function uploadFileWS() {} + /** clearAll() — очистить всё */ function clearAll() { if (uploading) return; diff --git a/wstest.yaml b/wstest.yaml new file mode 100644 index 0000000..92d1d60 --- /dev/null +++ b/wstest.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: Pod +metadata: + name: wstest + namespace: 9039a501-df2c-4b85-b049-9cd4f77e6ac0 + labels: + app: wstest +spec: + containers: + - name: app + image: python:3.12-slim + command: ["/bin/sh", "-c"] + args: + - | + pip install flask flask-sock && python -c ' + from flask import Flask + from flask_sock import Sock + import json, time + app = Flask(__name__) + sock = Sock(app) + + @sock.route("/ws") + def ws(ws): + t0 = time.time() + total = 0 + while True: + chunk = ws.receive() + if chunk is None: break + if isinstance(chunk, str) and chunk == "DONE": break + total += len(chunk) + ws.send(json.dumps({"ok":True,"bytes":total,"elapsed_ms":round((time.time()-t0)*1000)})) + + app.run(host="0.0.0.0", port=5000) + ' + ports: + - containerPort: 5000 + resources: + requests: {cpu: 100m, memory: 256Mi} + limits: {cpu: 500m, memory: 512Mi} +--- +apiVersion: v1 +kind: Service +metadata: + name: wstest + namespace: 9039a501-df2c-4b85-b049-9cd4f77e6ac0 +spec: + selector: + app: wstest + ports: + - port: 80 + targetPort: 5000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wstest + namespace: 9039a501-df2c-4b85-b049-9cd4f77e6ac0 + annotations: + nginx.ingress.kubernetes.io/proxy-request-buffering: "off" + nginx.ingress.kubernetes.io/proxy-buffering: "off" +spec: + rules: + - host: wstest.pythonk8s.dev.nubes.ru + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: wstest + port: + number: 80