api-admim/app/s3/RepositoryS3.py

272 lines
11 KiB
Python

# 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)}")