272 lines
11 KiB
Python
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)}")
|