# Importações de bibliotecas padrão from typing import Generic, TypeVar from uuid import uuid4, UUID import os # Importações de bibliotecas de terceiros from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from fastapi import HTTPException # Importações integração S3 import boto3 import app.config as config from starlette.responses import StreamingResponse from botocore.exceptions import ClientError from boto3.exceptions import Boto3Error from app.s3 import schema_s3 # Importações do seu próprio projeto from app.database.RepositoryBase import ( RepositoryBase, ) from app.database import models Model = TypeVar("Model", bound=models.Base) Schema = TypeVar("Schema", bound=BaseModel) s3_client = boto3.client( "s3", aws_access_key_id=config.S3_ACCESS_KEY_ID, aws_secret_access_key=config.S3_SECRET_ACCESS_KEY, endpoint_url=config.S3_ENDPOINT_URL, region_name=config.S3_REGION_NAME, ) def get_s3_path(inquilino: UUID, nome_arquivo: str) -> str: """ Gera o caminho completo no S3 para um arquivo de um inquilino específico. """ return f"{inquilino}/{nome_arquivo}" class RepositoryS3(RepositoryBase[Model], Generic[Model]): def __init__(self, model: type[Model], session: AsyncSession) -> None: super().__init__(model, session) async def get_file_record_by_uuid(self, uuid: UUID) -> models.S3Arquivo: """ Busca o registro de um arquivo pelo UUID no banco de dados. Levanta uma exceção HTTP 404 se o arquivo não for encontrado. """ try: # Chamar a função herdada para buscar o arquivo pelo UUID arquivo = await self.get_one_by_id(uuid, coluna="uuid") if not arquivo: raise HTTPException(status_code=404, detail="Arquivo não encontrado no banco de dados") return arquivo except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao buscar o arquivo no banco: {str(e)}") async def upload_to_s3(self, conteudo, nome_original, tipo_conteudo, inquilino: UUID): """ Faz upload do conteúdo para o S3 e salva apenas o nome do arquivo no banco. Apaga o arquivo do S3 em caso de erro ao salvar no banco. Agora o nome do arquivo é um UUID com a extensão do arquivo original. """ s3_path = None # Inicializa a variável no escopo superior try: # Obter a extensão do arquivo original _, file_extension = os.path.splitext(nome_original) file_extension = file_extension.lower() # Garantir que a extensão esteja em minúsculo # Gerar um nome único para o arquivo (UUID + extensão) unique_filename = f"{uuid4()}{file_extension}" # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, unique_filename) # Fazer upload do arquivo para o S3 s3_client.upload_fileobj( conteudo, config.S3_BUCKET_NAME, s3_path, ExtraArgs={ "ContentType": tipo_conteudo, # Nenhum metadado adicional salvo }, ) # Salvar apenas o nome do arquivo no banco arquivo_data = schema_s3.ArquivoCreate( arquivos_nome_original=nome_original, arquivos_nome_armazenado=unique_filename ) novo_arquivo = await self.create(arquivo_data) return novo_arquivo except Boto3Error as e: raise HTTPException(status_code=500, detail=f"Erro no S3: {str(e)}") except Exception as e: # Apagar o arquivo do S3 em caso de erro no banco de dados if s3_path: # Verifica se s3_path foi atribuído try: s3_client.delete_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path) except Exception as delete_error: print(f"Erro ao apagar arquivo do S3: {str(delete_error)}") # Relançar a exceção original raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}") async def get_presigned_url(self, uuid: UUID, inquilino: UUID) -> dict: """ Gera uma URL pré-assinada para download do arquivo, com o nome original configurado. """ try: # Buscar o registro do arquivo usando a função intermediária arquivo = await self.get_file_record_by_uuid(uuid) # Obter o nome armazenado e original do arquivo file_name = arquivo.arquivos_nome_armazenado original_filename = arquivo.arquivos_nome_original # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, file_name) # Gerar a URL pré-assinada para download presigned_url = s3_client.generate_presigned_url( "get_object", Params={ "Bucket": config.S3_BUCKET_NAME, "Key": s3_path, "ResponseContentDisposition": f'attachment; filename="{original_filename}"' }, ExpiresIn=3600, # URL válida por 1 hora ) return {"url": presigned_url} except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3") raise HTTPException(status_code=500, detail=f"Erro ao gerar URL: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}") async def generate_presigned_url(self, uuid: UUID, inquilino: UUID) -> dict: """ Gera uma URL pré-assinada para acessar o arquivo no S3 (sem download automático). """ try: # Buscar o registro do arquivo usando a função intermediária arquivo = await self.get_file_record_by_uuid(uuid) # Obter o nome armazenado do arquivo file_name = arquivo.arquivos_nome_armazenado # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, file_name) # Gerar uma URL pré-assinada presigned_url = s3_client.generate_presigned_url( "get_object", Params={ "Bucket": config.S3_BUCKET_NAME, "Key": s3_path }, ExpiresIn=3600, # URL válida por 1 hora ) return {"url": presigned_url} except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3") raise HTTPException(status_code=500, detail=f"Erro ao gerar URL: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}") async def get_file(self, uuid: UUID, inquilino: UUID) -> StreamingResponse: """ Retorna um arquivo específico para download com o nome original configurado. """ try: # Buscar o registro do arquivo usando a função intermediária arquivo = await self.get_file_record_by_uuid(uuid) # Obter o nome armazenado e original do arquivo file_name = arquivo.arquivos_nome_armazenado original_filename = arquivo.arquivos_nome_original # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, file_name) # Obter o objeto do S3 response = s3_client.get_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path) # Retornar o arquivo como um fluxo return StreamingResponse( response["Body"], media_type=response["ContentType"], # Tipo do conteúdo, ex.: image/jpeg headers={"Content-Disposition": f'attachment; filename="{original_filename}"'} ) except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3") raise HTTPException(status_code=500, detail=f"Erro ao acessar o arquivo: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}") async def get_file_inline(self, uuid: UUID, inquilino: UUID) -> StreamingResponse: """ Retorna um arquivo específico para exibição inline (sem download automático). """ try: # Buscar o registro do arquivo usando a função intermediária arquivo = await self.get_file_record_by_uuid(uuid) # Obter o nome armazenado do arquivo file_name = arquivo.arquivos_nome_armazenado # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, file_name) # Obter o objeto do S3 response = s3_client.get_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path) # Retornar o arquivo como um fluxo para exibição inline return StreamingResponse( response["Body"], media_type=response["ContentType"], # Tipo do conteúdo, ex.: image/jpeg ) except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3") raise HTTPException(status_code=500, detail=f"Erro ao acessar o arquivo: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}") async def delete_file_from_s3(self, uuid: UUID, inquilino: UUID): """ Remove um arquivo do S3 e o registro correspondente no banco de dados, usando o UUID como identificador. """ try: # Buscar o registro do arquivo usando o UUID arquivo = await self.get_file_record_by_uuid(uuid) # Obter o nome armazenado do arquivo file_name = arquivo.arquivos_nome_armazenado # Gerar o caminho completo no S3 s3_path = get_s3_path(inquilino, file_name) # Deletar o arquivo do S3 s3_client.delete_object(Bucket=config.S3_BUCKET_NAME, Key=s3_path) # Remover o registro do banco de dados result = await self.remove_by_id(uuid, coluna="uuid") return {"message": "Arquivo deletado com sucesso", "details": result} except ClientError as e: if e.response["Error"]["Code"] == "NoSuchKey": raise HTTPException(status_code=404, detail="Arquivo não encontrado no S3") raise HTTPException(status_code=500, detail=f"Erro ao deletar arquivo: {str(e)}") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")