uploader-upgrade
Some checks failed
Deploy on Push / deploy (push) Failing after 8s

This commit is contained in:
Untone 2024-05-06 12:00:57 +03:00
parent 3242758817
commit 8133d0030f
8 changed files with 248 additions and 91 deletions

View File

@ -1,12 +1,25 @@
FROM python:slim FROM python:alpine
# Update package lists and install necessary dependencies
RUN apk update &&
apk add --no-cache build-base icu-data-full curl python3-dev musl-dev &&
curl -sSL https://install.python-poetry.org | python
# Set working directory
WORKDIR /app WORKDIR /app
# Copy only the pyproject.toml file initially
COPY pyproject.toml /app/
# Install poetry and dependencies
RUN pip install poetry &&
poetry config virtualenvs.create false &&
poetry install --no-root --only main
# Copy the rest of the files
COPY . /app COPY . /app
RUN apt-get update && apt-get install -y git gcc curl postgresql && \ # Expose the port
curl -sSL https://install.python-poetry.org | python - && \ EXPOSE 8000
echo "export PATH=$PATH:/root/.local/bin" >> ~/.bashrc && \
. ~/.bashrc && \
poetry config virtualenvs.create false && \
poetry install --no-dev
CMD ["python", "main.py"] CMD ["python", "server.py"]

View File

@ -14,7 +14,7 @@
- STORJ_END_POINT - STORJ_END_POINT
- STORJ_BUCKET_NAME - STORJ_BUCKET_NAME
- CDN_DOMAIN - CDN_DOMAIN
- AUTH_URL
### Локальная разработка ### Локальная разработка
@ -23,7 +23,7 @@ mkdir .venv
python3.12 -m venv .venv python3.12 -m venv .venv
poetry env use .venv/bin/python3.12 poetry env use .venv/bin/python3.12
poetry update poetry update
poetry run python main.py poetry run python server.py
``` ```
### Интеграция в Core ### Интеграция в Core

91
auth.py
View File

@ -1,41 +1,72 @@
from functools import wraps from functools import wraps
from starlette.responses import JSONResponse
import aiohttp
AUTH_URL = 'https://auth.discours.io' import aiohttp
from starlette.responses import JSONResponse
from logger import root_logger as logger
from settings import AUTH_URL
async def request_data(gql, headers=None):
if headers is None:
headers = {"Content-Type": "application/json"}
try:
async with aiohttp.ClientSession() as session:
async with session.post(AUTH_URL, json=gql, headers=headers) as response:
if response.status == 200:
data = await response.json()
errors = data.get("errors")
if errors:
logger.error(f"HTTP Errors: {errors}")
else:
return data
except Exception as e:
# Handling and logging exceptions during authentication check
import traceback
logger.error(f"request_data error: {e}")
logger.error(traceback.format_exc())
return None
async def check_auth(req): async def check_auth(req):
token = req.headers.get("Authorization") token = req.headers.get("Authorization")
headers = {"Authorization": token, "Content-Type": "application/json"} user_id = ""
if token:
# Logging the authentication token
logger.debug(f"{token}")
query_name = "validate_jwt_token"
operation = "ValidateToken"
variables = {"params": {"token_type": "access_token", "token": token}}
print(f"[services.auth] checking auth token: {token}") gql = {
"query": f"query {operation}($params: ValidateJWTTokenInput!) {{"
+ f"{query_name}(params: $params) {{ is_valid claims }} "
+ "}",
"variables": variables,
"operationName": operation,
}
data = await request_data(gql)
if data:
logger.debug(data)
user_data = data.get("data", {}).get(query_name, {}).get("claims", {})
user_id = user_data.get("sub", "")
return user_id
gql = {
"query": "query GetUserId { session { user { id } } }",
"operationName": "GetUserId",
"variables": None,
}
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30.0)) as session:
async with session.post(AUTH_URL, headers=headers, json=gql) as response:
print(f"[services.auth] {AUTH_URL} response: {response.status}")
if response.status != 200:
return False, None
r = await response.json()
if r:
user_id = r.get("data", {}).get("session", {}).get("user", {}).get("id", None)
is_authenticated = user_id is not None
return is_authenticated, user_id
return False, None
def login_required(f): def login_required(f):
@wraps(f) """
async def decorated_function(request, *args, **kwargs): A decorator that requires user authentication before accessing a route.
is_authenticated, user_id = await check_auth(request) """
if not is_authenticated:
return JSONResponse({'error': 'Unauthorized'}, status_code=401) @wraps(f)
async def decorated_function(req, *args, **kwargs):
user_id = await check_auth(req)
if user_id:
logger.info(f" got {user_id}")
req.state.user_id = user_id.strip()
return await f(req, *args, **kwargs)
else:
return JSONResponse({"detail": "Not authorized"}, status_code=401)
# Make user_id available to the route handler, if needed
request.state.user_id = user_id
return await f(request, *args, **kwargs)
return decorated_function return decorated_function

80
logger.py Normal file
View File

@ -0,0 +1,80 @@
import logging
import colorlog
# Define the color scheme
color_scheme = {
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red,bg_white",
"DEFAULT": "white",
}
# Define secondary log colors
secondary_colors = {
"log_name": {"DEBUG": "blue"},
"asctime": {"DEBUG": "cyan"},
"process": {"DEBUG": "purple"},
"module": {"DEBUG": "cyan,bg_blue"},
"funcName": {"DEBUG": "light_white,bg_blue"},
}
# Define the log format string
fmt_string = "%(log_color)s%(levelname)s: %(log_color)s[%(module)s.%(funcName)s]%(reset)s %(white)s%(message)s"
# Define formatting configuration
fmt_config = {
"log_colors": color_scheme,
"secondary_log_colors": secondary_colors,
"style": "%",
"reset": True,
}
class MultilineColoredFormatter(colorlog.ColoredFormatter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log_colors = kwargs.pop("log_colors", {})
self.secondary_log_colors = kwargs.pop("secondary_log_colors", {})
def format(self, record):
message = record.getMessage()
if "\n" in message:
lines = message.split("\n")
first_line = lines[0]
record.message = first_line
formatted_first_line = super().format(record)
formatted_lines = [formatted_first_line]
for line in lines[1:]:
formatted_lines.append(line)
return "\n".join(formatted_lines)
else:
return super().format(record)
# Create a MultilineColoredFormatter object for colorized logging
formatter = MultilineColoredFormatter(fmt_string, **fmt_config)
# Create a stream handler for logging output
stream = logging.StreamHandler()
stream.setFormatter(formatter)
# Set up the root logger with the same formatting
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(stream)
ignore_logs = [
"_trace",
"httpx",
"_client",
"_trace.atrace",
"aiohttp",
"_client",
"._make_request",
]
for lgr in ignore_logs:
loggr = logging.getLogger(lgr)
loggr.setLevel(logging.INFO)

53
main.py
View File

@ -7,35 +7,43 @@ from starlette.applications import Starlette
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.routing import Route from starlette.routing import Route
from starlette.requests import Request from starlette.requests import Request
from auth import check_auth from auth import login_required
from settings import (
PORT,
STORJ_ACCESS_KEY,
STORJ_SECRET_KEY,
CDN_DOMAIN,
STORJ_BUCKET_NAME,
STORJ_END_POINT,
)
# Logging configuration # Logging configuration
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(
level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s"
)
STORJ_ACCESS_KEY = os.environ.get('STORJ_ACCESS_KEY')
STORJ_SECRET_KEY = os.environ.get('STORJ_SECRET_KEY')
STORJ_END_POINT = os.environ.get('STORJ_END_POINT')
STORJ_BUCKET_NAME = os.environ.get('STORJ_BUCKET_NAME')
CDN_DOMAIN = os.environ.get('CDN_DOMAIN')
@check_auth @login_required
async def upload_handler(request: Request): async def upload_handler(request: Request):
logging.debug("Received upload request") logging.debug("Received upload request")
form = await request.form() form = await request.form()
file = form.get('file') file = form.get("file")
if file is None: if file is None:
logging.error("No file uploaded") logging.error("No file uploaded")
return JSONResponse({'error': 'No file uploaded'}, status_code=400) return JSONResponse({"error": "No file uploaded"}, status_code=400)
file_name, file_extension = os.path.splitext(file.filename) file_name, file_extension = os.path.splitext(file.filename)
key = str(uuid.uuid4()) + file_extension key = str(uuid.uuid4()) + file_extension
logging.debug(f"Generated file key: {key}") logging.debug(f"Generated file key: {key}")
async with aioboto3.client('s3', async with aioboto3.client(
aws_access_key_id=STORJ_ACCESS_KEY, "s3",
aws_secret_access_key=STORJ_SECRET_KEY, aws_access_key_id=STORJ_ACCESS_KEY,
endpoint_url=STORJ_END_POINT) as s3: aws_secret_access_key=STORJ_SECRET_KEY,
endpoint_url=STORJ_END_POINT,
) as s3:
with tempfile.NamedTemporaryFile() as tmp_file: with tempfile.NamedTemporaryFile() as tmp_file:
while True: while True:
chunk = await file.read(8192) chunk = await file.read(8192)
@ -49,25 +57,28 @@ async def upload_handler(request: Request):
Filename=tmp_file.name, Filename=tmp_file.name,
Bucket=STORJ_BUCKET_NAME, Bucket=STORJ_BUCKET_NAME,
Key=key, Key=key,
ExtraArgs={"ContentType": file.content_type} ExtraArgs={"ContentType": file.content_type},
) )
logging.debug("File upload completed") logging.debug("File upload completed")
url = f'http://{CDN_DOMAIN}/{key}' url = f"http://{CDN_DOMAIN}/{key}"
logging.info(f"File uploaded successfully: {url}") logging.info(f"File uploaded successfully: {url}")
return JSONResponse({'url': url, 'originalFilename': file.filename}) return JSONResponse({"url": url, "originalFilename": file.filename})
async def home(request: Request): async def home(request: Request):
logging.debug("Home route called") logging.debug("Home route called")
return JSONResponse({'message': 'Hello World!'}) return JSONResponse({"message": "Hello World!"})
routes = [ routes = [
Route('/test', home, methods=['GET']), Route("/test", home, methods=["GET"]),
Route('/', upload_handler, methods=['POST']) Route("/", upload_handler, methods=["POST"]),
] ]
app = Starlette(debug=True, routes=routes) app = Starlette(debug=True, routes=routes)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host='0.0.0.0', port=8080)
uvicorn.run(app, host="0.0.0.0", port=PORT)

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "discoursio-migrator" name = "discoursio-migrator"
version = "0.2.6" version = "0.3.0"
description = "" description = ""
authors = ["discoursio devteam"] authors = ["discoursio devteam"]
license = "MIT" license = "MIT"
@ -8,40 +8,27 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
aiohttp = "^3.9.1"
uvicorn = "^0.24.0.post1"
starlette = "^0.33.0" starlette = "^0.33.0"
aioboto3 = "^9.0.0" aioboto3 = "^9.0.0"
python-multipart = "^0.0.5" python-multipart = "^0.0.5"
colorlog = "^6.8.2"
[tool.poetry.dev-dependencies] granian = "^1.3.1"
black = "^23.10.1" aiohttp = "^3.9.5"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
setuptools = "^69.0.2" ruff = "^0.3.5"
isort = "^5.13.2"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.black] [tool.pyright]
line-length = 120 venvPath = "."
target-version = ['py312'] venv = ".venv"
include = '\.pyi?$'
exclude = ''' [tool.isort]
( multi_line_output = 3
/( include_trailing_comma = true
\.eggs force_grid_wrap = 0
| \.git line_length = 120
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
| foo.py
)
'''

26
server.py Normal file
View File

@ -0,0 +1,26 @@
from granian.constants import Interfaces
from granian.server import Granian
import subprocess
from logger import root_logger as logger
from settings import PORT
def is_docker_container_running(name):
cmd = ["docker", "ps", "-f", f"name={name}"]
output = subprocess.run(cmd, capture_output=True, text=True).stdout
logger.info(output)
return name in output
if __name__ == "__main__":
logger.info("started")
granian_instance = Granian(
"main:app",
address="0.0.0.0", # noqa S104
port=PORT,
threads=4,
websockets=False,
interface=Interfaces.ASGI,
)
granian_instance.serve()

9
settings.py Normal file
View File

@ -0,0 +1,9 @@
from os import environ
PORT = 8000
AUTH_URL = environ.get("AUTH_URL") or ""
STORJ_ACCESS_KEY = environ.get("STORJ_ACCESS_KEY")
STORJ_SECRET_KEY = environ.get("STORJ_SECRET_KEY")
STORJ_END_POINT = environ.get("STORJ_END_POINT")
STORJ_BUCKET_NAME = environ.get("STORJ_BUCKET_NAME")
CDN_DOMAIN = environ.get("CDN_DOMAIN")