Spaces:
Sleeping
Sleeping
| """ | |
| File Naming Utility | |
| Generates contextual, self-documenting filenames for object storage. | |
| Makes files easily identifiable even if database is lost. | |
| Naming Pattern: | |
| {entity_type}_{entity_short_id}_{document_type}_{timestamp}_{original_name} | |
| Examples: | |
| - ticket_8f08ad14_completion_photo_speedtest_20251130_105718.png | |
| - user_43b778b0_profile_photo_20251130_105718.jpg | |
| - expense_decafa10_receipt_20251130_105718.pdf | |
| - project_0ade6bd1_contract_20251130_105718.pdf | |
| """ | |
| from datetime import datetime | |
| from uuid import UUID | |
| import re | |
| from pathlib import Path | |
| class FileNamingService: | |
| """Service for generating contextual filenames""" | |
| def sanitize_filename(filename: str) -> str: | |
| """ | |
| Sanitize filename to be filesystem and URL safe | |
| - Remove special characters | |
| - Replace spaces with underscores | |
| - Lowercase | |
| - Keep only alphanumeric, underscore, hyphen, dot | |
| """ | |
| # Get filename without extension | |
| stem = Path(filename).stem | |
| ext = Path(filename).suffix | |
| # Remove special characters, keep alphanumeric, underscore, hyphen | |
| stem = re.sub(r'[^\w\-]', '_', stem) | |
| # Replace multiple underscores with single | |
| stem = re.sub(r'_+', '_', stem) | |
| # Lowercase and strip | |
| stem = stem.lower().strip('_') | |
| # Limit length to 50 chars | |
| if len(stem) > 50: | |
| stem = stem[:50] | |
| return f"{stem}{ext.lower()}" | |
| def get_short_id(entity_id: UUID) -> str: | |
| """ | |
| Get short version of UUID for filename (first 8 chars) | |
| Example: 8f08ad14-df8b-4780-84e7-0d45e133f2a6 -> 8f08ad14 | |
| """ | |
| return str(entity_id).split('-')[0] | |
| def get_timestamp() -> str: | |
| """ | |
| Get timestamp for filename in format: YYYYMMDD_HHMMSS | |
| Example: 20251130_105718 | |
| """ | |
| return datetime.utcnow().strftime('%Y%m%d_%H%M%S') | |
| def generate_contextual_filename( | |
| entity_type: str, | |
| entity_id: UUID, | |
| document_type: str, | |
| original_filename: str, | |
| additional_context: str = None | |
| ) -> str: | |
| """ | |
| Generate contextual, self-documenting filename | |
| Args: | |
| entity_type: Type of entity (ticket, user, expense, etc.) | |
| entity_id: UUID of the entity | |
| document_type: Type of document (completion_photo, receipt, profile_photo, etc.) | |
| original_filename: Original uploaded filename | |
| additional_context: Optional additional context (photo_type, etc.) | |
| Returns: | |
| Contextual filename | |
| Examples: | |
| ticket_8f08ad14_completion_photo_speedtest_20251130_105718.png | |
| user_43b778b0_profile_photo_20251130_105718.jpg | |
| expense_decafa10_receipt_fuel_20251130_105718.pdf | |
| """ | |
| # Get components | |
| short_id = FileNamingService.get_short_id(entity_id) | |
| timestamp = FileNamingService.get_timestamp() | |
| sanitized_original = FileNamingService.sanitize_filename(original_filename) | |
| # Remove extension from original for now | |
| original_stem = Path(sanitized_original).stem | |
| extension = Path(sanitized_original).suffix | |
| # Build filename parts | |
| parts = [ | |
| entity_type.lower(), | |
| short_id, | |
| document_type.lower().replace(' ', '_') | |
| ] | |
| # Add additional context if provided | |
| if additional_context: | |
| sanitized_context = re.sub(r'[^\w\-]', '_', additional_context.lower()) | |
| parts.append(sanitized_context) | |
| # Add timestamp | |
| parts.append(timestamp) | |
| # Add original filename (without extension) if it's meaningful | |
| # Skip if it's generic like "image", "photo", "file", "screenshot" | |
| generic_names = ['image', 'photo', 'file', 'screenshot', 'document', 'img', 'pic'] | |
| if original_stem and original_stem.lower() not in generic_names: | |
| # Limit original name to 30 chars | |
| if len(original_stem) > 30: | |
| original_stem = original_stem[:30] | |
| parts.append(original_stem) | |
| # Join parts and add extension | |
| filename = '_'.join(parts) + extension | |
| return filename | |
| def generate_ticket_photo_filename( | |
| ticket_id: UUID, | |
| photo_type: str, | |
| original_filename: str | |
| ) -> str: | |
| """ | |
| Generate filename for ticket completion photos | |
| Example: ticket_8f08ad14_completion_photo_speedtest_20251130_105718.png | |
| """ | |
| return FileNamingService.generate_contextual_filename( | |
| entity_type='ticket', | |
| entity_id=ticket_id, | |
| document_type='completion_photo', | |
| original_filename=original_filename, | |
| additional_context=photo_type | |
| ) | |
| def generate_expense_receipt_filename( | |
| expense_id: UUID, | |
| expense_category: str, | |
| original_filename: str | |
| ) -> str: | |
| """ | |
| Generate filename for expense receipts | |
| Example: expense_decafa10_receipt_fuel_20251130_105718.pdf | |
| """ | |
| return FileNamingService.generate_contextual_filename( | |
| entity_type='expense', | |
| entity_id=expense_id, | |
| document_type='receipt', | |
| original_filename=original_filename, | |
| additional_context=expense_category | |
| ) | |
| def generate_user_document_filename( | |
| user_id: UUID, | |
| document_type: str, | |
| original_filename: str | |
| ) -> str: | |
| """ | |
| Generate filename for user documents | |
| Examples: | |
| user_43b778b0_profile_photo_20251130_105718.jpg | |
| user_43b778b0_id_document_20251130_105718.pdf | |
| """ | |
| return FileNamingService.generate_contextual_filename( | |
| entity_type='user', | |
| entity_id=user_id, | |
| document_type=document_type, | |
| original_filename=original_filename | |
| ) | |
| def generate_project_document_filename( | |
| project_id: UUID, | |
| document_type: str, | |
| original_filename: str | |
| ) -> str: | |
| """ | |
| Generate filename for project documents | |
| Example: project_0ade6bd1_contract_20251130_105718.pdf | |
| """ | |
| return FileNamingService.generate_contextual_filename( | |
| entity_type='project', | |
| entity_id=project_id, | |
| document_type=document_type, | |
| original_filename=original_filename | |
| ) | |