cad2program / utils /file_upload_handler.py
turiya-ai's picture
Create file_upload_handler.py
90a850b verified
Raw
History Blame Contribute Delete
22.9 kB
"""
Enhanced File Upload Handler for CAD2Program
Handles various file types, batch uploads, and file validation
"""
import os
import tempfile
import zipfile
import tarfile
import json
import shutil
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any, Union
import logging
from PIL import Image, ImageOps
import io
import base64
import mimetypes
logger = logging.getLogger(__name__)
class FileUploadHandler:
"""Enhanced file upload handler with validation and processing"""
def __init__(self, upload_dir: str = "uploads", max_file_size: int = 50 * 1024 * 1024):
self.upload_dir = Path(upload_dir)
self.upload_dir.mkdir(exist_ok=True)
self.max_file_size = max_file_size # 50MB default
# Supported file types
self.supported_image_types = {'.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp'}
self.supported_program_types = {'.py', '.txt'}
self.supported_archive_types = {'.zip', '.tar', '.tar.gz', '.tgz'}
self.supported_cad_types = {'.dxf', '.dwg', '.step', '.stp', '.stl', '.obj'}
def validate_file(self, file_path: Union[str, Path]) -> Dict[str, Any]:
"""Validate uploaded file"""
file_path = Path(file_path)
validation_result = {
"valid": False,
"file_type": None,
"size": 0,
"errors": [],
"warnings": []
}
try:
# Check if file exists
if not file_path.exists():
validation_result["errors"].append("File does not exist")
return validation_result
# Check file size
file_size = file_path.stat().st_size
validation_result["size"] = file_size
if file_size > self.max_file_size:
validation_result["errors"].append(f"File too large: {file_size / 1024 / 1024:.1f}MB > {self.max_file_size / 1024 / 1024:.1f}MB")
return validation_result
if file_size == 0:
validation_result["errors"].append("File is empty")
return validation_result
# Check file extension
file_extension = file_path.suffix.lower()
if file_extension in self.supported_image_types:
validation_result["file_type"] = "image"
# Validate image
try:
with Image.open(file_path) as img:
validation_result["image_info"] = {
"format": img.format,
"mode": img.mode,
"size": img.size
}
# Check image dimensions
if img.size[0] < 64 or img.size[1] < 64:
validation_result["warnings"].append("Image resolution is very low")
elif img.size[0] > 4096 or img.size[1] > 4096:
validation_result["warnings"].append("Image resolution is very high, will be resized")
except Exception as e:
validation_result["errors"].append(f"Invalid image file: {str(e)}")
return validation_result
elif file_extension in self.supported_program_types:
validation_result["file_type"] = "program"
# Validate program file
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
validation_result["program_info"] = {
"lines": len(content.split('\n')),
"characters": len(content),
"has_primitives": "add_primitive" in content or "Primitive" in content
}
except Exception as e:
validation_result["errors"].append(f"Cannot read program file: {str(e)}")
return validation_result
elif file_extension in self.supported_archive_types:
validation_result["file_type"] = "archive"
# Validate archive
try:
if file_extension == '.zip':
with zipfile.ZipFile(file_path, 'r') as zf:
validation_result["archive_info"] = {
"format": "zip",
"files": len(zf.namelist()),
"file_list": zf.namelist()[:10] # First 10 files
}
elif file_extension in {'.tar', '.tar.gz', '.tgz'}:
with tarfile.open(file_path, 'r') as tf:
validation_result["archive_info"] = {
"format": "tar",
"files": len(tf.getnames()),
"file_list": tf.getnames()[:10] # First 10 files
}
except Exception as e:
validation_result["errors"].append(f"Invalid archive file: {str(e)}")
return validation_result
elif file_extension in self.supported_cad_types:
validation_result["file_type"] = "cad"
validation_result["cad_info"] = {
"format": file_extension[1:].upper(),
"note": "CAD file detected - may need special processing"
}
else:
validation_result["errors"].append(f"Unsupported file type: {file_extension}")
return validation_result
# If we get here, file is valid
validation_result["valid"] = True
except Exception as e:
validation_result["errors"].append(f"Validation error: {str(e)}")
return validation_result
def process_single_file(self, file_data: Any, filename: str) -> Dict[str, Any]:
"""Process a single uploaded file"""
# Create unique filename to avoid conflicts
timestamp = str(int(os.path.getmtime(filename)) if os.path.exists(filename) else 0)
safe_filename = self._sanitize_filename(filename)
unique_filename = f"{timestamp}_{safe_filename}"
file_path = self.upload_dir / unique_filename
try:
# Save file
if hasattr(file_data, 'read'):
# File-like object
with open(file_path, 'wb') as f:
f.write(file_data.read())
elif isinstance(file_data, bytes):
# Bytes data
with open(file_path, 'wb') as f:
f.write(file_data)
else:
# Assume it's a path
shutil.copy2(file_data, file_path)
# Validate file
validation = self.validate_file(file_path)
if not validation["valid"]:
# Clean up invalid file
if file_path.exists():
file_path.unlink()
return {
"success": False,
"filename": filename,
"errors": validation["errors"],
"validation": validation
}
# Process based on file type
processed_data = self._process_by_type(file_path, validation)
return {
"success": True,
"filename": filename,
"saved_path": str(file_path),
"validation": validation,
"processed_data": processed_data
}
except Exception as e:
logger.error(f"Error processing file {filename}: {e}")
return {
"success": False,
"filename": filename,
"errors": [f"Processing error: {str(e)}"]
}
def process_batch_upload(self, files: List[Tuple[Any, str]]) -> Dict[str, Any]:
"""Process multiple files at once"""
results = {
"successful": [],
"failed": [],
"summary": {
"total": len(files),
"success_count": 0,
"error_count": 0,
"images": 0,
"programs": 0,
"archives": 0
}
}
for file_data, filename in files:
result = self.process_single_file(file_data, filename)
if result["success"]:
results["successful"].append(result)
results["summary"]["success_count"] += 1
# Count file types
file_type = result["validation"]["file_type"]
if file_type in results["summary"]:
results["summary"][file_type + "s"] += 1
else:
results["failed"].append(result)
results["summary"]["error_count"] += 1
return results
def process_archive(self, archive_path: Union[str, Path]) -> Dict[str, Any]:
"""Extract and process archive contents"""
archive_path = Path(archive_path)
extract_dir = self.upload_dir / f"extracted_{archive_path.stem}"
extract_dir.mkdir(exist_ok=True)
extracted_files = []
try:
# Extract archive
if archive_path.suffix.lower() == '.zip':
with zipfile.ZipFile(archive_path, 'r') as zf:
zf.extractall(extract_dir)
extracted_files = [extract_dir / name for name in zf.namelist()]
elif archive_path.suffix.lower() in {'.tar', '.tar.gz', '.tgz'}:
with tarfile.open(archive_path, 'r') as tf:
tf.extractall(extract_dir)
extracted_files = [extract_dir / name for name in tf.getnames()]
# Process extracted files
processed_results = []
for extracted_file in extracted_files:
if extracted_file.is_file():
validation = self.validate_file(extracted_file)
if validation["valid"]:
processed_data = self._process_by_type(extracted_file, validation)
processed_results.append({
"file_path": str(extracted_file),
"validation": validation,
"processed_data": processed_data
})
return {
"success": True,
"extract_dir": str(extract_dir),
"extracted_count": len(extracted_files),
"processed_count": len(processed_results),
"results": processed_results
}
except Exception as e:
return {
"success": False,
"error": f"Archive processing failed: {str(e)}"
}
def _process_by_type(self, file_path: Path, validation: Dict) -> Dict[str, Any]:
"""Process file based on its type"""
file_type = validation["file_type"]
processed_data = {"type": file_type}
if file_type == "image":
processed_data.update(self._process_image(file_path, validation))
elif file_type == "program":
processed_data.update(self._process_program(file_path, validation))
elif file_type == "archive":
processed_data.update(self.process_archive(file_path))
elif file_type == "cad":
processed_data.update(self._process_cad_file(file_path, validation))
return processed_data
def _process_image(self, file_path: Path, validation: Dict) -> Dict[str, Any]:
"""Process uploaded image file"""
try:
with Image.open(file_path) as img:
# Convert to RGB if necessary
if img.mode != 'RGB':
img = img.convert('RGB')
# Resize if too large
max_size = 1024
if img.size[0] > max_size or img.size[1] > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# Save resized image
resized_path = file_path.parent / f"resized_{file_path.name}"
img.save(resized_path, 'PNG')
return {
"original_size": validation["image_info"]["size"],
"resized_path": str(resized_path),
"new_size": img.size,
"format": "PNG"
}
return {
"size": img.size,
"format": img.format,
"mode": img.mode,
"processed": True
}
except Exception as e:
return {"error": f"Image processing failed: {str(e)}"}
def _process_program(self, file_path: Path, validation: Dict) -> Dict[str, Any]:
"""Process uploaded program file"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Analyze program content
analysis = {
"lines": len(content.split('\n')),
"characters": len(content),
"primitives": [],
"imports": [],
"functions": []
}
# Extract primitives
for line in content.split('\n'):
line = line.strip()
if 'add_primitive' in line:
analysis["primitives"].append(line)
elif line.startswith('import ') or line.startswith('from '):
analysis["imports"].append(line)
elif line.startswith('def '):
analysis["functions"].append(line)
return {
"analysis": analysis,
"content_preview": content[:500], # First 500 characters
"processed": True
}
except Exception as e:
return {"error": f"Program processing failed: {str(e)}"}
def _process_cad_file(self, file_path: Path, validation: Dict) -> Dict[str, Any]:
"""Process CAD files (placeholder for future implementation)"""
return {
"format": validation["cad_info"]["format"],
"note": "CAD file processing not yet implemented",
"future_feature": True
}
def create_dataset_from_uploads(self, upload_results: Dict) -> Dict[str, Any]:
"""Create a dataset from processed uploads"""
dataset_entries = []
for result in upload_results["successful"]:
validation = result["validation"]
processed = result["processed_data"]
if validation["file_type"] == "image":
# Look for matching program file
image_path = Path(result["saved_path"])
base_name = image_path.stem
# Try to find matching program
program_path = None
for prog_result in upload_results["successful"]:
if prog_result["validation"]["file_type"] == "program":
prog_path = Path(prog_result["saved_path"])
if prog_path.stem == base_name:
program_path = prog_path
break
if program_path:
# Read program content
with open(program_path, 'r') as f:
program_content = f.read()
entry = {
"id": f"upload_{base_name}",
"image_path": str(image_path),
"program_path": str(program_path),
"program_text": program_content,
"source": "user_upload",
"validation": validation,
"processed_data": processed
}
dataset_entries.append(entry)
# Save dataset
dataset_file = self.upload_dir / "created_dataset.json"
with open(dataset_file, 'w') as f:
json.dump(dataset_entries, f, indent=2)
return {
"dataset_file": str(dataset_file),
"entries": len(dataset_entries),
"pairs_found": len(dataset_entries)
}
def _sanitize_filename(self, filename: str) -> str:
"""Sanitize filename for safe storage"""
# Remove path separators and dangerous characters
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")
sanitized = "".join(c if c in safe_chars else "_" for c in filename)
# Limit length
if len(sanitized) > 100:
name, ext = os.path.splitext(sanitized)
sanitized = name[:100-len(ext)] + ext
return sanitized
def cleanup_old_uploads(self, days: int = 7):
"""Clean up old uploaded files"""
import time
cutoff_time = time.time() - (days * 24 * 60 * 60)
cleaned_files = 0
for file_path in self.upload_dir.rglob('*'):
if file_path.is_file():
try:
if file_path.stat().st_mtime < cutoff_time:
file_path.unlink()
cleaned_files += 1
except Exception as e:
logger.warning(f"Failed to clean up {file_path}: {e}")
return {"cleaned_files": cleaned_files}
def get_upload_stats(self) -> Dict[str, Any]:
"""Get statistics about uploads"""
stats = {
"total_files": 0,
"total_size": 0,
"file_types": {},
"recent_uploads": []
}
for file_path in self.upload_dir.rglob('*'):
if file_path.is_file():
stats["total_files"] += 1
file_size = file_path.stat().st_size
stats["total_size"] += file_size
# Count by extension
ext = file_path.suffix.lower()
stats["file_types"][ext] = stats["file_types"].get(ext, 0) + 1
# Track recent uploads (last 24 hours)
import time
if file_path.stat().st_mtime > time.time() - 86400:
stats["recent_uploads"].append({
"name": file_path.name,
"size": file_size,
"modified": file_path.stat().st_mtime
})
return stats
# Gradio-specific upload handlers
def create_gradio_upload_interface():
"""Create Gradio interface components for file uploads"""
import gradio as gr
upload_handler = FileUploadHandler()
def handle_gradio_upload(files):
"""Handle file upload from Gradio interface"""
if not files:
return "No files uploaded", ""
# Process files
file_tuples = []
for file in files:
file_tuples.append((file.file, file.name))
results = upload_handler.process_batch_upload(file_tuples)
# Format results for display
summary = f"""
## Upload Results
**Total Files:** {results['summary']['total']}
**Successful:** {results['summary']['success_count']}
**Failed:** {results['summary']['error_count']}
**File Types:**
- Images: {results['summary']['images']}
- Programs: {results['summary']['programs']}
- Archives: {results['summary']['archives']}
"""
# Create dataset if we have matching pairs
dataset_info = ""
if results['summary']['success_count'] > 1:
dataset = upload_handler.create_dataset_from_uploads(results)
dataset_info = f"\n**Dataset Created:** {dataset['entries']} entries"
return summary + dataset_info, str(results)
# Define Gradio components
file_upload = gr.File(
label="Upload CAD Files",
file_count="multiple",
file_types=[".png", ".jpg", ".jpeg", ".py", ".txt", ".zip"]
)
upload_btn = gr.Button("Process Uploads", variant="primary")
results_display = gr.Markdown(label="Upload Results")
detailed_results = gr.JSON(label="Detailed Results", visible=False)
upload_btn.click(
fn=handle_gradio_upload,
inputs=[file_upload],
outputs=[results_display, detailed_results]
)
return file_upload, upload_btn, results_display, detailed_results
# Utility functions for integration with main app
def setup_upload_system(base_dir: str = "uploads") -> FileUploadHandler:
"""Setup upload system with proper directory structure"""
handler = FileUploadHandler(base_dir)
# Create subdirectories
(Path(base_dir) / "images").mkdir(exist_ok=True)
(Path(base_dir) / "programs").mkdir(exist_ok=True)
(Path(base_dir) / "archives").mkdir(exist_ok=True)
(Path(base_dir) / "processed").mkdir(exist_ok=True)
return handler
if __name__ == "__main__":
# Test upload handler
handler = setup_upload_system("test_uploads")
print("Upload handler created")
print(f"Upload directory: {handler.upload_dir}")
# Test file validation
test_extensions = ['.png', '.py', '.zip', '.invalid']
for ext in test_extensions:
# Create dummy file for testing
test_file = handler.upload_dir / f"test{ext}"
test_file.write_text("test content")
validation = handler.validate_file(test_file)
print(f"{ext}: Valid={validation['valid']}, Type={validation.get('file_type', 'unknown')}")
# Clean up
test_file.unlink()
# Get stats
stats = handler.get_upload_stats()
print(f"Upload stats: {stats}")