swiftops-backend / src /app /utils /file_naming.py
kamau1's picture
feat: file naming
b26895a
"""
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"""
@staticmethod
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()}"
@staticmethod
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]
@staticmethod
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')
@staticmethod
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
@staticmethod
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
)
@staticmethod
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
)
@staticmethod
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
)
@staticmethod
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
)