-
Notifications
You must be signed in to change notification settings - Fork 35
β‘ Bolt: optimize field officer image upload performance #706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||||||
| """ | ||||||||||
|
|
||||||||||
| from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Response | ||||||||||
| from fastapi.concurrency import run_in_threadpool | ||||||||||
| from sqlalchemy.orm import Session | ||||||||||
| from sqlalchemy import func, case | ||||||||||
| from typing import List, Optional | ||||||||||
|
|
@@ -34,6 +35,7 @@ | |||||||||
| ) | ||||||||||
| from backend.cache import visit_last_hash_cache, visit_stats_cache | ||||||||||
| from backend.schemas import BlockchainVerificationResponse | ||||||||||
| from backend.utils import process_uploaded_image, save_processed_image | ||||||||||
|
|
||||||||||
| logger = logging.getLogger(__name__) | ||||||||||
|
|
||||||||||
|
|
@@ -301,55 +303,49 @@ async def upload_visit_images( | |||||||||
| ) | ||||||||||
|
|
||||||||||
| image_paths = [] | ||||||||||
|
|
||||||||||
| for idx, image in enumerate(images): | ||||||||||
| # Validate content_type is present | ||||||||||
| if not image.content_type: | ||||||||||
| raise HTTPException(status_code=400, detail="File must have a content type") | ||||||||||
|
|
||||||||||
| # Validate file type | ||||||||||
| if not image.content_type.startswith('image/'): | ||||||||||
| raise HTTPException(status_code=400, detail=f"File must be an image, got {image.content_type}") | ||||||||||
|
|
||||||||||
| # Validate filename is present | ||||||||||
| if not image.filename: | ||||||||||
| raise HTTPException(status_code=400, detail="File must have a filename") | ||||||||||
|
|
||||||||||
| # Validate extension | ||||||||||
| extension = image.filename.split('.')[-1].lower() if '.' in image.filename else '' | ||||||||||
| # Validate extension (Strict check to maintain original behavior) | ||||||||||
| if not image.filename or '.' not in image.filename: | ||||||||||
| raise HTTPException(status_code=400, detail="Invalid filename") | ||||||||||
|
|
||||||||||
| extension = image.filename.split('.')[-1].lower() | ||||||||||
| if extension not in ALLOWED_IMAGE_EXTENSIONS: | ||||||||||
| raise HTTPException( | ||||||||||
| status_code=400, | ||||||||||
| detail=f"File extension '{extension}' not allowed. Allowed: {', '.join(ALLOWED_IMAGE_EXTENSIONS)}" | ||||||||||
| detail=f"File extension '{extension}' not allowed." | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| # Read and validate file size | ||||||||||
| content = await image.read() | ||||||||||
| if len(content) > MAX_UPLOAD_SIZE: | ||||||||||
|
|
||||||||||
| # Optimization: Unified validation, resizing, and EXIF stripping in one pass. | ||||||||||
| # This avoids multiple redundant decode/encode cycles and ensures optimal storage. | ||||||||||
| _, image_bytes = await process_uploaded_image(image) | ||||||||||
|
|
||||||||||
|
Comment on lines
+319
to
+322
|
||||||||||
| # Enforce stricter 10MB limit for this specific endpoint (original constraint) | ||||||||||
| if len(image_bytes) > MAX_UPLOAD_SIZE: | ||||||||||
| raise HTTPException( | ||||||||||
| status_code=400, | ||||||||||
| detail=f"File {image.filename} exceeds maximum size of {MAX_UPLOAD_SIZE / 1024 / 1024:.1f} MB" | ||||||||||
| status_code=413, | ||||||||||
| detail=f"Processed image exceeds {MAX_UPLOAD_SIZE / (1024*1024):.0f}MB limit" | ||||||||||
| ) | ||||||||||
|
Comment on lines
+319
to
328
|
||||||||||
|
|
||||||||||
| # Generate secure filename | ||||||||||
| timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') | ||||||||||
| safe_filename = f"visit_{visit_id}_{timestamp}_{idx}.{extension}" | ||||||||||
| file_path = os.path.join(VISIT_IMAGES_DIR, safe_filename) | ||||||||||
|
|
||||||||||
| # Save file | ||||||||||
| with open(file_path, 'wb') as f: | ||||||||||
| f.write(content) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| # Performance Boost: Offload blocking File I/O to threadpool | ||||||||||
| await run_in_threadpool(save_processed_image, image_bytes, file_path) | ||||||||||
|
|
||||||||||
| # Store relative path | ||||||||||
| relative_path = os.path.join("data", "visit_images", safe_filename) | ||||||||||
| image_paths.append(relative_path) | ||||||||||
|
|
||||||||||
| # Update visit with image paths | ||||||||||
| existing_images.extend(image_paths) | ||||||||||
| visit.visit_images = existing_images | ||||||||||
| visit.updated_at = datetime.now(timezone.utc) | ||||||||||
|
|
||||||||||
| db.commit() | ||||||||||
|
|
||||||||||
| # Performance Boost: Offload blocking DB commit to threadpool | ||||||||||
| await run_in_threadpool(db.commit) | ||||||||||
|
Comment on lines
+335
to
+348
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# Description: Verify SQLAlchemy session configuration for thread safety.
# Check how get_db is implemented
rg -nA 10 'def get_db' backend/database.py backend/ 2>/dev/null || rg -nA 10 'def get_db' --type=py
# Check for scoped_session usage
rg -n 'scoped_session' --type=pyRepository: RohanExploit/VishwaGuru Length of output: 538 SQLAlchemy session is not thread-safe; The Instead, either:
π€ Prompt for AI Agents
Comment on lines
+347
to
+348
|
||||||||||
| # Performance Boost: Offload blocking DB commit to threadpool | |
| await run_in_threadpool(db.commit) | |
| # Keep all SQLAlchemy Session usage on the same thread. | |
| db.commit() |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The 10MB limit is now enforced only after processing, so raw files up to 20MB may pass initial validation and violate this endpointβs original upload-size contract.
Prompt for AI agents