From 64445eb793facec859d81e38fc010a35b4f12580 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 27 Feb 2026 11:35:58 -0500 Subject: [PATCH] Feb 27 2026: fix upload size --- app/__init__.py | 20 ++++++ app/templates/inspections/execute.html | 98 ++++++++++++++++++++++++++ config.py | 2 +- 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index c7aa0e5..a78b917 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -44,6 +44,26 @@ def create_app(config_name='default'): app.register_blueprint(facilities.bp) app.register_blueprint(issues.bp) + # ── Error handler: 413 Request Entity Too Large ─────────────────────── + # Nginx can return 413 before Flask sees the request; this handler covers + # the Flask-side rejection and gives users a clear, actionable message + # with a redirect back into the inspection workflow. + from werkzeug.exceptions import RequestEntityTooLarge + + @app.errorhandler(RequestEntityTooLarge) + @app.errorhandler(413) + def handle_413(e): + from flask import request as flask_request, flash as flask_flash, redirect, url_for + flask_flash( + f'The uploaded file(s) are too large. ' + f'Please reduce the photo size or upload fewer photos at once ' + f'(maximum {app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024)}MB per submission).', + 'danger' + ) + # Redirect back to the referring page if available, otherwise dashboard + referrer = flask_request.referrer + return redirect(referrer or url_for('dashboard.index')), 302 + with app.app_context(): db.create_all() diff --git a/app/templates/inspections/execute.html b/app/templates/inspections/execute.html index 2ec11c0..8eb8a7d 100644 --- a/app/templates/inspections/execute.html +++ b/app/templates/inspections/execute.html @@ -496,5 +496,103 @@ function collectSignatures() { function confirmSubmit() { return confirm('Submit this inspection? This action cannot be undone.'); } + +// ── Client-side image compression ───────────────────────────────────────────── +// Compresses photos via Canvas before upload to prevent 413 errors. +// Target: ≤ 1MB per image at 85% JPEG quality, max 1920px on longest side. +const IMG_MAX_PX = 1920; // max dimension in pixels +const IMG_QUALITY = 0.85; // JPEG quality (0–1) +const IMG_MAX_BYTES = 1 * 1024 * 1024; // 1MB per compressed image + +function compressImageFile(file) { + return new Promise((resolve) => { + // Only compress images that exceed the size threshold + if (!file.type.startsWith('image/') || file.size <= IMG_MAX_BYTES) { + return resolve(file); + } + + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + // Calculate scaled dimensions + let { width, height } = img; + if (width > IMG_MAX_PX || height > IMG_MAX_PX) { + if (width >= height) { + height = Math.round(height * IMG_MAX_PX / width); + width = IMG_MAX_PX; + } else { + width = Math.round(width * IMG_MAX_PX / height); + height = IMG_MAX_PX; + } + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.getContext('2d').drawImage(img, 0, 0, width, height); + + canvas.toBlob((blob) => { + // Create a new File from the compressed blob, preserving the name + const compressed = new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() }); + resolve(compressed); + }, 'image/jpeg', IMG_QUALITY); + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); + }); +} + +// Intercept form submission — compress all image fields before sending +document.getElementById('inspectionForm').addEventListener('submit', async function(e) { + const fileInputs = this.querySelectorAll('input[type="file"]'); + if (!fileInputs.length) return; // nothing to compress + + // Only intercept if there are files to compress + const hasFiles = Array.from(fileInputs).some(inp => inp.files && inp.files.length > 0); + if (!hasFiles) return; + + e.preventDefault(); + + const btn = this.querySelector('button[type="submit"]'); + const origText = btn ? btn.innerHTML : ''; + if (btn) { + btn.disabled = true; + btn.innerHTML = ' Compressing photos…'; + } + + try { + // Build a FormData object with compressed images + const formData = new FormData(this); + + for (const input of fileInputs) { + if (!input.files || !input.files.length) continue; + const compressed = await compressImageFile(input.files[0]); + formData.set(input.name, compressed, compressed.name); + } + + // Submit via fetch with the compressed FormData + const response = await fetch(this.action || window.location.href, { + method: 'POST', + body: formData, + credentials: 'same-origin', + }); + + if (response.redirected) { + window.location.href = response.url; + } else if (response.ok) { + window.location.reload(); + } else { + // Surface server error (e.g. still-too-large after compression) + if (btn) { btn.disabled = false; btn.innerHTML = origText; } + alert('Submission failed (HTTP ' + response.status + '). Please try again or remove some photos.'); + } + } catch (err) { + if (btn) { btn.disabled = false; btn.innerHTML = origText; } + console.error('Photo compression error:', err); + alert('An error occurred while processing photos. Please try again.'); + } +}); {% endblock %} diff --git a/config.py b/config.py index d5355c7..4f13d27 100644 --- a/config.py +++ b/config.py @@ -33,7 +33,7 @@ class Config: # ── File uploads ──────────────────────────────────────────────────────── UPLOAD_FOLDER = os.path.join(basedir, 'app/static/uploads') - MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16 MB + MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 16 MB ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} # ── Session / cookies ───────────────────────────────────────────────────