Skip to content
This repository was archived by the owner on Apr 10, 2026. It is now read-only.

Commit b50ec0b

Browse files
Mihai - Alexandru ChindrișMihai - Alexandru Chindriș
authored andcommitted
fix: auto-compress large browser uploads and accept csv.gz
1 parent c80f730 commit b50ec0b

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

src/web/app.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,20 @@ def upload():
263263
if not file or file.filename == "":
264264
return render_template("upload.html", error="No file selected.")
265265

266+
filename = file.filename.lower()
267+
if not (filename.endswith(".csv") or filename.endswith(".csv.gz")):
268+
return render_template(
269+
"upload.html",
270+
error="Unsupported file type. Please upload a .csv or .csv.gz file.",
271+
)
272+
compression = "gzip" if filename.endswith(".gz") else None
273+
266274
# Stream CSV in chunks to keep memory bounded for large uploads.
267-
chunks = pd.read_csv(file.stream, chunksize=UPLOAD_CHUNK_SIZE)
275+
chunks = pd.read_csv(
276+
file.stream,
277+
chunksize=UPLOAD_CHUNK_SIZE,
278+
compression=compression,
279+
)
268280
first_chunk = next(chunks, None)
269281
if first_chunk is None or first_chunk.empty:
270282
return render_template("upload.html", error="Uploaded CSV is empty.")

src/web/templates/upload.html

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ <h1>Batch Prediction</h1>
1414
<form method="POST" action="/upload" enctype="multipart/form-data">
1515
<div class="form-group">
1616
<label for="file">Select CSV file:</label>
17-
<input type="file" name="file" id="file" accept=".csv" required>
17+
<input type="file" name="file" id="file" accept=".csv,.gz" required>
1818
</div>
19+
<p id="upload-status" aria-live="polite"></p>
1920
<button type="submit" class="btn btn-primary">Upload & Predict</button>
2021
</form>
2122

@@ -74,4 +75,69 @@ <h2>Predictions ({{ num_predictions }} instances)</h2>
7475
</div>
7576
</div>
7677
{% endif %}
78+
79+
<script>
80+
(() => {
81+
const form = document.querySelector('form[action="/upload"]');
82+
const fileInput = document.getElementById('file');
83+
const statusEl = document.getElementById('upload-status');
84+
const LARGE_FILE_THRESHOLD = 8 * 1024 * 1024;
85+
86+
if (!form || !fileInput || !statusEl) {
87+
return;
88+
}
89+
90+
const setStatus = (text) => {
91+
statusEl.textContent = text;
92+
};
93+
94+
form.addEventListener('submit', async (event) => {
95+
const file = fileInput.files && fileInput.files[0];
96+
if (!file) {
97+
return;
98+
}
99+
100+
if (file.size < LARGE_FILE_THRESHOLD || typeof CompressionStream === 'undefined') {
101+
return;
102+
}
103+
104+
event.preventDefault();
105+
const submitButton = form.querySelector('button[type="submit"]');
106+
if (submitButton) {
107+
submitButton.disabled = true;
108+
}
109+
110+
try {
111+
setStatus('Large file detected. Compressing before upload...');
112+
const gzipStream = file.stream().pipeThrough(new CompressionStream('gzip'));
113+
const compressedBlob = await new Response(gzipStream).blob();
114+
const gzipFile = new File([compressedBlob], file.name + '.gz', {
115+
type: 'application/gzip',
116+
lastModified: Date.now(),
117+
});
118+
119+
const formData = new FormData();
120+
formData.append('file', gzipFile);
121+
122+
setStatus('Uploading compressed file...');
123+
const response = await fetch('/upload', {
124+
method: 'POST',
125+
body: formData,
126+
});
127+
128+
const html = await response.text();
129+
document.open();
130+
document.write(html);
131+
document.close();
132+
} catch (_error) {
133+
setStatus('Compression failed. Falling back to regular upload...');
134+
form.submit();
135+
} finally {
136+
if (submitButton) {
137+
submitButton.disabled = false;
138+
}
139+
}
140+
});
141+
})();
142+
</script>
77143
{% endblock %}

tests/test_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import json
44
import re
55
from pathlib import Path
6+
import gzip
7+
import io
68

79
import pytest
810
from src.web.app import app
@@ -71,3 +73,23 @@ def test_upload_csv_and_download_results(client):
7173
assert download_response.status_code == 200
7274
assert "attachment" in download_response.headers.get("Content-Disposition", "")
7375
assert b"Prediction" in download_response.data
76+
77+
78+
def test_upload_gzipped_csv(client):
79+
"""Test batch upload accepts .csv.gz files."""
80+
csv_path = Path(__file__).resolve().parents[1] / "data" / "test_set.csv"
81+
82+
with csv_path.open("rb") as f:
83+
raw = f.read()
84+
compressed = gzip.compress(raw)
85+
file_obj = io.BytesIO(compressed)
86+
87+
response = client.post(
88+
"/upload",
89+
data={"file": (file_obj, "test_set.csv.gz")},
90+
content_type="multipart/form-data",
91+
)
92+
93+
assert response.status_code == 200
94+
html = response.data.decode("utf-8")
95+
assert "Predictions (" in html

0 commit comments

Comments
 (0)