Skip to content

Commit 590ffb6

Browse files
committed
feature: user avatar upload route added & file upload task added
1 parent ace1a52 commit 590ffb6

File tree

6 files changed

+100
-2
lines changed

6 files changed

+100
-2
lines changed

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ services:
9494
container_name: minio
9595
ports:
9696
- "9090:9090"
97+
- "9000:9000"
9798
volumes:
9899
- minio_data:/data
99100
env_file:

pkg/storage.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from minio import Minio
2+
3+
from .config import Config
4+
5+
minio_client = Minio(
6+
endpoint=Config.MINIO_STORAGE_ENDPOINT,
7+
access_key=Config.MINIO_ACCESS_KEY,
8+
secret_key=Config.MINIO_SECRET_KEY,
9+
secure=False,
10+
)
11+
12+
if not minio_client.bucket_exists(Config.MINIO_STORAGE_BUCKET):
13+
minio_client.make_bucket(Config.MINIO_STORAGE_BUCKET)
14+
print(f"Bucket {Config.MINIO_STORAGE_BUCKET} created")

requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ alembic==1.13.3
33
amqp==5.2.0
44
annotated-types==0.7.0
55
anyio==4.6.0
6+
argon2-cffi==23.1.0
7+
argon2-cffi-bindings==21.2.0
68
asgiref==3.8.1
79
asyncpg==0.29.0
810
bcrypt==4.2.0
911
billiard==4.2.1
1012
celery==5.4.0
1113
certifi==2024.8.30
14+
cffi==1.17.1
1215
click==8.1.7
1316
click-didyoumean==0.3.1
1417
click-plugins==1.1.1
@@ -32,9 +35,12 @@ Mako==1.3.5
3235
markdown-it-py==3.0.0
3336
MarkupSafe==2.1.5
3437
mdurl==0.1.2
38+
minio==7.2.9
3539
prometheus_client==0.21.0
3640
prompt_toolkit==3.0.48
3741
psycopg2-binary==2.9.9
42+
pycparser==2.22
43+
pycryptodome==3.21.0
3844
pydantic==2.9.2
3945
pydantic-settings==2.5.2
4046
pydantic_core==2.23.4
@@ -57,6 +63,7 @@ tornado==6.4.1
5763
typer==0.12.5
5864
typing_extensions==4.12.2
5965
tzdata==2024.2
66+
urllib3==2.2.3
6067
uvicorn==0.31.0
6168
uvloop==0.20.0
6269
vine==5.1.0

src/profile/routes.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import json
2+
import os
23

3-
from fastapi import APIRouter, Depends, status
4+
from fastapi import APIRouter, Depends, Request, status
45
from fastapi.responses import JSONResponse
56
from sqlmodel.ext.asyncio.session import AsyncSession
67

8+
from pkg.config import Config
79
from pkg.db import get_session
810
from pkg.utils import get_current_user_uid
911
from src.auth.service import UserService
1012

1113
from .schemas import UserProfileResponseSchema, UserProfileUpdateSchema
1214
from .service import UserProfileService
15+
from .tasks import upload_user_avatar_image_task
1316

1417
profile_router = APIRouter()
1518

1619
user_service = UserService()
1720
user_profile_service = UserProfileService()
1821

1922

20-
@profile_router.put("/", status_code=status.HTTP_200_OK)
23+
@profile_router.patch("/update-profile", status_code=status.HTTP_200_OK)
2124
async def update_user_profile(
2225
profile_data: UserProfileUpdateSchema,
2326
session: AsyncSession = Depends(get_session),
@@ -39,3 +42,48 @@ async def update_user_profile(
3942
).model_dump(),
4043
},
4144
)
45+
46+
47+
@profile_router.patch("/update-avatar", status_code=status.HTTP_200_OK)
48+
async def update_user_avatar(
49+
request: Request,
50+
session: AsyncSession = Depends(get_session),
51+
user_uid: str = Depends(get_current_user_uid),
52+
):
53+
user_profile = await user_profile_service.get_user_profile_by_user_uid(
54+
user_uid, session
55+
)
56+
57+
form = await request.form()
58+
avatar_image = form.get("avatar_image")
59+
60+
if not avatar_image:
61+
return JSONResponse(
62+
status_code=status.HTTP_400_BAD_REQUEST,
63+
content={"message": "Avatar image is required"},
64+
)
65+
66+
avatar_image_content = await avatar_image.read()
67+
avatar_image_file_extension = os.path.splitext(avatar_image.filename)[1]
68+
avatar_image_file_name = f"avatars/{user_uid}{avatar_image_file_extension}"
69+
70+
upload_user_avatar_image_task.delay(
71+
avatar_image_content,
72+
avatar_image_file_name,
73+
avatar_image.content_type,
74+
)
75+
76+
file_url = f"http://{Config.MINIO_STORAGE_ENDPOINT}/{Config.MINIO_STORAGE_BUCKET}/{avatar_image_file_name}"
77+
await user_profile_service.update_user_profile_avatar(
78+
user_profile, file_url, session
79+
)
80+
81+
return JSONResponse(
82+
status_code=status.HTTP_200_OK,
83+
content={
84+
"message": "User avatar updated successfully",
85+
"user_profile": UserProfileResponseSchema(
86+
**json.loads(user_profile.model_dump_json())
87+
).model_dump(),
88+
},
89+
)

src/profile/service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ async def get_user_profile_by_user_uid(
2424
)
2525
user_profile = result.scalars().first()
2626
return user_profile
27+
28+
async def update_user_profile_avatar(
29+
self, user_profile: UserProfile, user_avatar_url: str, session: AsyncSession
30+
):
31+
user_profile.avatar = user_avatar_url
32+
await session.commit()
33+
await session.refresh(user_profile)
34+
return user_profile

src/profile/tasks.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from io import BytesIO
2+
3+
from pkg.celery_app import celery_app
4+
from pkg.config import Config
5+
from pkg.storage import minio_client
6+
7+
8+
@celery_app.task
9+
def upload_user_avatar_image_task(
10+
image_content: bytes, filename: str, content_type: str
11+
):
12+
file_data = BytesIO(image_content)
13+
14+
minio_client.put_object(
15+
bucket_name=Config.MINIO_STORAGE_BUCKET,
16+
object_name=filename,
17+
data=file_data,
18+
length=len(image_content),
19+
content_type=content_type,
20+
)

0 commit comments

Comments
 (0)