232 lines
6.6 KiB
Python
232 lines
6.6 KiB
Python
|
|
"""Service for managing file attachments to entities"""
|
||
|
|
|
||
|
|
from io import BytesIO
|
||
|
|
from typing import List, Optional
|
||
|
|
|
||
|
|
from flask import current_app
|
||
|
|
|
||
|
|
from app.models import Card, Comment, FileAttachment
|
||
|
|
from app.services.storage.file_service import FileService
|
||
|
|
|
||
|
|
|
||
|
|
class AttachmentService:
|
||
|
|
"""Service for managing file attachments to Cards, Comments, etc."""
|
||
|
|
|
||
|
|
def __init__(self, file_service: Optional[FileService] = None):
|
||
|
|
"""
|
||
|
|
Initialize AttachmentService with dependency injection
|
||
|
|
|
||
|
|
Args:
|
||
|
|
file_service: FileService instance
|
||
|
|
(defaults to new instance with dependency injection)
|
||
|
|
"""
|
||
|
|
self.file_service = file_service or FileService()
|
||
|
|
|
||
|
|
def attach_file_to_card(
|
||
|
|
self,
|
||
|
|
card_id: int,
|
||
|
|
file_data: BytesIO,
|
||
|
|
filename: str,
|
||
|
|
content_type: str,
|
||
|
|
file_size: int,
|
||
|
|
uploaded_by: int,
|
||
|
|
) -> FileAttachment:
|
||
|
|
"""
|
||
|
|
Attach a file to a card
|
||
|
|
|
||
|
|
Args:
|
||
|
|
card_id: ID of the card
|
||
|
|
file_data: File data as BytesIO
|
||
|
|
filename: Original filename
|
||
|
|
content_type: MIME type
|
||
|
|
file_size: File size in bytes
|
||
|
|
uploaded_by: User ID who uploaded the file
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
FileAttachment: Created attachment record
|
||
|
|
"""
|
||
|
|
# Verify card exists
|
||
|
|
card = Card.query.get(card_id)
|
||
|
|
if not card:
|
||
|
|
raise ValueError(f"Card with ID {card_id} not found")
|
||
|
|
|
||
|
|
# Upload file
|
||
|
|
return self.file_service.upload_file(
|
||
|
|
file_data=file_data,
|
||
|
|
filename=filename,
|
||
|
|
content_type=content_type,
|
||
|
|
file_size=file_size,
|
||
|
|
uploaded_by=uploaded_by,
|
||
|
|
attachable_type="Card",
|
||
|
|
attachable_id=card_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
def attach_file_to_comment(
|
||
|
|
self,
|
||
|
|
comment_id: int,
|
||
|
|
file_data: BytesIO,
|
||
|
|
filename: str,
|
||
|
|
content_type: str,
|
||
|
|
file_size: int,
|
||
|
|
uploaded_by: int,
|
||
|
|
) -> FileAttachment:
|
||
|
|
"""
|
||
|
|
Attach a file to a comment
|
||
|
|
|
||
|
|
Args:
|
||
|
|
comment_id: ID of the comment
|
||
|
|
file_data: File data as BytesIO
|
||
|
|
filename: Original filename
|
||
|
|
content_type: MIME type
|
||
|
|
file_size: File size in bytes
|
||
|
|
uploaded_by: User ID who uploaded the file
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
FileAttachment: Created attachment record
|
||
|
|
"""
|
||
|
|
# Verify comment exists
|
||
|
|
comment = Comment.query.get(comment_id)
|
||
|
|
if not comment:
|
||
|
|
raise ValueError(f"Comment with ID {comment_id} not found")
|
||
|
|
|
||
|
|
# Upload file
|
||
|
|
return self.file_service.upload_file(
|
||
|
|
file_data=file_data,
|
||
|
|
filename=filename,
|
||
|
|
content_type=content_type,
|
||
|
|
file_size=file_size,
|
||
|
|
uploaded_by=uploaded_by,
|
||
|
|
attachable_type="Comment",
|
||
|
|
attachable_id=comment_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
def attach_file_to_entity(
|
||
|
|
self,
|
||
|
|
entity_type: str,
|
||
|
|
entity_id: int,
|
||
|
|
file_data: BytesIO,
|
||
|
|
filename: str,
|
||
|
|
content_type: str,
|
||
|
|
file_size: int,
|
||
|
|
uploaded_by: int,
|
||
|
|
) -> FileAttachment:
|
||
|
|
"""
|
||
|
|
Attach a file to any entity type
|
||
|
|
|
||
|
|
Args:
|
||
|
|
entity_type: Type of entity (Card, Comment, etc.)
|
||
|
|
entity_id: ID of the entity
|
||
|
|
file_data: File data as BytesIO
|
||
|
|
filename: Original filename
|
||
|
|
content_type: MIME type
|
||
|
|
file_size: File size in bytes
|
||
|
|
uploaded_by: User ID who uploaded the file
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
FileAttachment: Created attachment record
|
||
|
|
"""
|
||
|
|
# Validate entity type
|
||
|
|
valid_types = ["Card", "Comment", "Epic", "Board"]
|
||
|
|
if entity_type not in valid_types:
|
||
|
|
raise ValueError(
|
||
|
|
f"Invalid entity type: {entity_type}. Must be one of {valid_types}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Upload file
|
||
|
|
return self.file_service.upload_file(
|
||
|
|
file_data=file_data,
|
||
|
|
filename=filename,
|
||
|
|
content_type=content_type,
|
||
|
|
file_size=file_size,
|
||
|
|
uploaded_by=uploaded_by,
|
||
|
|
attachable_type=entity_type,
|
||
|
|
attachable_id=entity_id,
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_card_attachments(self, card_id: int) -> List[FileAttachment]:
|
||
|
|
"""
|
||
|
|
Get all attachments for a card
|
||
|
|
|
||
|
|
Args:
|
||
|
|
card_id: ID of the card
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
list: List of FileAttachment objects
|
||
|
|
"""
|
||
|
|
return self.file_service.get_files_for_entity("Card", card_id)
|
||
|
|
|
||
|
|
def get_comment_attachments(self, comment_id: int) -> List[FileAttachment]:
|
||
|
|
"""
|
||
|
|
Get all attachments for a comment
|
||
|
|
|
||
|
|
Args:
|
||
|
|
comment_id: ID of the comment
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
list: List of FileAttachment objects
|
||
|
|
"""
|
||
|
|
return self.file_service.get_files_for_entity("Comment", comment_id)
|
||
|
|
|
||
|
|
def get_entity_attachments(
|
||
|
|
self, entity_type: str, entity_id: int
|
||
|
|
) -> List[FileAttachment]:
|
||
|
|
"""
|
||
|
|
Get all attachments for any entity type
|
||
|
|
|
||
|
|
Args:
|
||
|
|
entity_type: Type of entity (Card, Comment, etc.)
|
||
|
|
entity_id: ID of the entity
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
list: List of FileAttachment objects
|
||
|
|
"""
|
||
|
|
return self.file_service.get_files_for_entity(entity_type, entity_id)
|
||
|
|
|
||
|
|
def get_attachment_with_url(self, attachment_id: int) -> dict:
|
||
|
|
"""
|
||
|
|
Get attachment with download URL
|
||
|
|
|
||
|
|
Args:
|
||
|
|
attachment_id: ID of the attachment
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
dict: Attachment information with URLs
|
||
|
|
"""
|
||
|
|
return self.file_service.get_file_info(attachment_id)
|
||
|
|
|
||
|
|
def delete_attachment(self, attachment_id: int) -> bool:
|
||
|
|
"""
|
||
|
|
Delete an attachment
|
||
|
|
|
||
|
|
Args:
|
||
|
|
attachment_id: ID of the attachment
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if successful
|
||
|
|
"""
|
||
|
|
return self.file_service.delete_file(attachment_id)
|
||
|
|
|
||
|
|
def delete_entity_attachments(self, entity_type: str, entity_id: int) -> int:
|
||
|
|
"""
|
||
|
|
Delete all attachments for an entity (cascade delete)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
entity_type: Type of entity (Card, Comment, etc.)
|
||
|
|
entity_id: ID of the entity
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
int: Number of attachments deleted
|
||
|
|
"""
|
||
|
|
attachments = self.get_entity_attachments(entity_type, entity_id)
|
||
|
|
deleted_count = 0
|
||
|
|
|
||
|
|
for attachment in attachments:
|
||
|
|
if self.delete_attachment(attachment.id):
|
||
|
|
deleted_count += 1
|
||
|
|
|
||
|
|
current_app.logger.info(
|
||
|
|
f"Deleted {deleted_count} attachments for {entity_type} {entity_id}"
|
||
|
|
)
|
||
|
|
|
||
|
|
return deleted_count
|