Feb 27 2026: fix upload size
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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 = '<span class="spinner-border spinner-border-sm me-1"></span> 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.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user