66from contextlib import asynccontextmanager
77from datetime import datetime , timedelta
88from typing import List
9+
10+ import aiofiles
911from fastapi import FastAPI , Depends , HTTPException , Path , Body , status
12+ from sqlalchemy .orm import Session
1013from sqlalchemy import func
11- from sqlalchemy .orm import Session , joinedload
12- from sqlalchemy import exists
13- from database import SessionLocal , engine , Base
14+ from database import SessionLocal , engine
1415import models
1516import schemas
1617import crud
@@ -152,10 +153,22 @@ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db:
152153@app .get ("/users/me" , response_model = schemas .User )
153154def read_users_me (token : str = Depends (oauth2_scheme ), db : Session = Depends (get_db )):
154155 username = get_username_from_token (token )
155- user = crud .get_user_by_username (db , username = username )
156- if user is None :
156+ user_data = (
157+ db .query (
158+ models .User ,
159+ func .sum (models .Item .bonus ).label ('item_power' )
160+ )
161+ .outerjoin (models .User .items )
162+ .filter (models .User .username == username )
163+ .group_by (models .User .id )
164+ .first ()
165+ )
166+
167+ if not user_data :
157168 raise HTTPException (status_code = 401 , detail = "User not found" )
158- user .power = user .power + sum (item .bonus for item in user .items )
169+
170+ user , item_power = user_data
171+ user .power = user .power + (item_power or 0 )
159172 return user
160173
161174
@@ -171,32 +184,47 @@ def create_item_for_user(item: schemas.ItemCreate, token: str = Depends(oauth2_s
171184@app .get ("/users/" , response_model = list [schemas .User ])
172185def read_users (
173186 db : Session = Depends (get_db ),
174- limit : int = 30
187+ limit : int = 30 ,
188+ offset : int = 0
175189):
176- zehn_min_ago = datetime .utcnow () - timedelta (minutes = 10 )
177- users = (
178- db .query (models .User )
179- .options (joinedload (models .User .items ))
180- .all ()
190+ users_query = (
191+ db .query (
192+ models .User ,
193+ func .sum (models .Item .bonus ).label ('item_power' )
194+ )
195+ .outerjoin (models .User .items )
196+ .group_by (models .User .id )
197+ .order_by (models .User .created_at .desc ())
198+ .offset (offset )
199+ .limit (limit )
181200 )
182- user_dicts = []
183- for user in users :
184- base_power = user .power
185- items_power = sum (item .bonus for item in user .items )
186- user_dict = user .__dict__ .copy ()
187- user_dict ["power" ] = base_power + items_power
188- user_dict ["items" ] = []
189- user_dicts .append (user_dict )
190- users = sorted (user_dicts , key = lambda u : u ["created_at" ], reverse = True )
191- return users [:limit ]
201+
202+ users_with_power = users_query .all ()
203+
204+ result_users = []
205+ for user , item_power in users_with_power :
206+ user .power = user .power + (item_power or 0 )
207+ result_users .append (user )
208+ return result_users
192209
193210
194211@app .get ("/users/{user_id}" , response_model = schemas .User )
195212def get_user_by_id (user_id : int = Path (...), db : Session = Depends (get_db )):
196- user = crud .get_user_by_id (db , user_id )
197- if not user :
213+ user_data = (
214+ db .query (
215+ models .User ,
216+ func .sum (models .Item .bonus ).label ('item_power' )
217+ )
218+ .outerjoin (models .User .items )
219+ .filter (models .User .id == user_id )
220+ .group_by (models .User .id )
221+ .first ()
222+ )
223+ if not user_data :
198224 raise HTTPException (status_code = 404 , detail = "User not found" )
199- user .power = user .power + sum (item .bonus for item in user .items )
225+
226+ user , item_power = user_data
227+ user .power = user .power + (item_power or 0 )
200228 user .items = []
201229 return user
202230
@@ -207,7 +235,9 @@ def open_lootbox(token: str = Depends(oauth2_scheme), db: Session = Depends(get_
207235 user = crud .get_user_by_username (db , username = username )
208236 if not user :
209237 raise HTTPException (status_code = 401 , detail = "User not found" )
210- items_count = len (user .items )
238+
239+ items_count = db .query (func .count (models .Inventory .item_id )).filter (models .Inventory .user_id == user .id ).scalar ()
240+
211241 if items_count >= 2 :
212242 raise HTTPException (status_code = 403 , detail = "Max. 2 Lootboxes per User!" )
213243
@@ -248,9 +278,15 @@ def update_item_note(
248278):
249279 username = get_username_from_token (token )
250280 user = crud .get_user_by_username (db , username = username )
251- user_items = [i .id for i in user .items ]
252- if item_id not in user_items :
253- raise HTTPException (status_code = 403 , detail = "You are not allowed to edit the not of this Item!" )
281+
282+ item_owned = db .query (models .Inventory ).filter (
283+ models .Inventory .user_id == user .id ,
284+ models .Inventory .item_id == item_id
285+ ).first () is not None
286+
287+ if not item_owned :
288+ raise HTTPException (status_code = 403 , detail = "You are not allowed to edit the note of this Item, or the item does not exist." )
289+
254290 item = crud .update_item_note (db , item_id , note )
255291 if item == "locked" :
256292 raise HTTPException (status_code = 403 , detail = "Item note can only be set once and cannot be changed or deleted!" )
@@ -275,14 +311,26 @@ def fight(defender_id: int, token: str = Depends(oauth2_scheme), db: Session = D
275311 if not attacker or not defender :
276312 raise HTTPException (status_code = 404 , detail = "User not found" )
277313
278- def calc_power (user ):
279- return user .power + sum (item .bonus for item in user .items )
314+ def calc_power_db (user_id : int , db_session : Session ):
315+ power_data = (
316+ db_session .query (
317+ models .User .power ,
318+ func .sum (models .Item .bonus ).label ('item_power' )
319+ )
320+ .outerjoin (models .User .items )
321+ .filter (models .User .id == user_id )
322+ .group_by (models .User .id )
323+ .first ()
324+ )
325+ if not power_data :
326+ return 0
327+ base_power , item_power = power_data
328+ return base_power + (item_power or 0 )
280329
281- attacker_power = calc_power (attacker )
282- defender_power = calc_power (defender )
330+ attacker_power = calc_power_db (attacker . id , db )
331+ defender_power = calc_power_db (defender . id , db )
283332
284333 winner = attacker if attacker_power >= defender_power else defender
285- loser = defender if winner == attacker else attacker
286334
287335 xp_win = 20
288336 xp_lose = 5
@@ -483,7 +531,13 @@ def enter_dungeon(dungeon_id: int, current_user: schemas.User = Depends(get_curr
483531 if dungeon .one_time_clear and crud .has_completed_dungeon (db , current_user .id , dungeon_id ):
484532 raise HTTPException (status_code = 400 , detail = "This dungeon can only be cleared once." )
485533
486- user_power_with_items = current_user .power + sum (item .bonus for item in current_user .items )
534+ power_data = db .query (
535+ models .User .power ,
536+ func .sum (models .Item .bonus ).label ('item_bonus_sum' )
537+ ).outerjoin (models .User .items ).filter (models .User .id == current_user .id ).group_by (models .User .id ).first ()
538+
539+ base_power , item_bonus = power_data
540+ user_power_with_items = base_power + (item_bonus or 0 )
487541
488542 new_level = None
489543
@@ -515,16 +569,25 @@ def enter_dungeon(dungeon_id: int, current_user: schemas.User = Depends(get_curr
515569
516570
517571async def cleanup_old_records ():
572+ """
573+ Periodically cleans up old records from the database and deletes associated
574+ user upload folders asynchronously.
575+ """
518576 while True :
519- await asyncio .sleep (60 )
577+ await asyncio .sleep (120 )
520578 db = SessionLocal ()
579+ loop = asyncio .get_event_loop ()
521580 try :
522581 ten_minutes_ago = datetime .utcnow () - timedelta (minutes = 10 )
523582
524- users_to_delete = db .query (models .User ).filter (models .User .created_at < ten_minutes_ago ).all ()
525- deleted_usernames = [user .username for user in users_to_delete ]
583+ users_to_delete_query = db .query (models .User .username ).filter (
584+ models .User .created_at < ten_minutes_ago
585+ )
586+ deleted_usernames = [u [0 ] for u in users_to_delete_query .all ()]
526587
527- db .query (models .Fight ).filter (models .Fight .created_at < ten_minutes_ago ).delete (synchronize_session = False )
588+ db .query (models .Fight ).filter (
589+ models .Fight .created_at < ten_minutes_ago
590+ ).delete (synchronize_session = False )
528591
529592 dungeons_to_clear_subquery = db .query (models .Dungeon .id ).filter (
530593 (models .Dungeon .one_time_clear == False ) | (models .Dungeon .one_time_clear == None )
@@ -535,8 +598,13 @@ async def cleanup_old_records():
535598 models .CompletedDungeon .dungeon_id .in_ (dungeons_to_clear_subquery )
536599 ).delete (synchronize_session = False )
537600
538- db .query (models .User ).filter (models .User .created_at < ten_minutes_ago ).delete (synchronize_session = False )
539- db .query (models .Item ).filter (models .Item .created_at < ten_minutes_ago ).delete (synchronize_session = False )
601+ db .query (models .User ).filter (
602+ models .User .created_at < ten_minutes_ago
603+ ).delete (synchronize_session = False )
604+
605+ db .query (models .Item ).filter (
606+ models .Item .created_at < ten_minutes_ago
607+ ).delete (synchronize_session = False )
540608
541609 db .query (models .Dungeon ).filter (
542610 models .Dungeon .created_at < ten_minutes_ago ,
@@ -549,7 +617,7 @@ async def cleanup_old_records():
549617 user_dir = os .path .join ("uploads" , username )
550618 if os .path .exists (user_dir ):
551619 try :
552- shutil .rmtree ( user_dir )
620+ await loop . run_in_executor ( None , shutil .rmtree , user_dir )
553621 print (f"Deleted upload folder for user { username } " )
554622 except Exception as e :
555623 print (f"Failed to delete upload folder for user { username } : { e } " )
@@ -569,38 +637,24 @@ async def upload_dungeon_image(file: UploadFile = File(...),
569637 if not file .content_type .startswith ("image/" ):
570638 raise HTTPException (status_code = 400 , detail = "Only image files are allowed." )
571639
572- user_upload_dir = f"uploads/{ current_user .username } "
573- os .makedirs (user_upload_dir , exist_ok = True )
574-
575- file_path = os .path .join (user_upload_dir , file .filename )
576-
577- if ".." in file .filename or "/" in file .filename :
640+ safe_filename = os .path .basename (file .filename )
641+ if not safe_filename or ".." in safe_filename or "/" in safe_filename :
578642 raise HTTPException (status_code = 400 , detail = "Invalid filename." )
579643
580- if os .path .exists (file_path ):
581- raise HTTPException (status_code = 400 , detail = "File with this name already exists for the user." )
582-
583- with open (file_path , "wb" ) as buffer :
584- shutil .copyfileobj (file .file , buffer )
585-
586- return {"filename" : file .filename , "detail" : "Image uploaded successfully to user directory." }
587-
588-
589- @app .get ("/images" , response_model = schemas .ImageList )
590- def get_dungeon_images (current_user : schemas .User = Depends (get_current_active_user )):
591- default_images = []
592- user_images = []
644+ user_upload_dir = f"uploads/{ current_user .username } "
645+ file_path = os .path .join (user_upload_dir , safe_filename )
593646
594- default_dir = "uploads/default"
595- user_dir = f"uploads/ { current_user . username } "
647+ loop = asyncio . get_event_loop ()
648+ await loop . run_in_executor ( None , os . makedirs , user_upload_dir , 0o755 , True )
596649
597- if os .path .exists ( default_dir ):
598- default_images = [ f for f in os . listdir ( default_dir ) if os . path . isfile ( os . path . join ( default_dir , f ))]
650+ if await loop . run_in_executor ( None , os .path .exists , file_path ):
651+ raise HTTPException ( status_code = 400 , detail = "File with this name already exists for the user." )
599652
600- if os .path .exists (user_dir ):
601- user_images = [f for f in os .listdir (user_dir ) if os .path .isfile (os .path .join (user_dir , f ))]
653+ async with aiofiles .open (file_path , "wb" ) as buffer :
654+ content = await file .read ()
655+ await buffer .write (content )
602656
603- return {"default " : default_images , "user " : user_images }
657+ return {"filename " : safe_filename , "detail " : "Image uploaded successfully to user directory." }
604658
605659
606660@app .get ("/images/{filename:path}" )
@@ -610,4 +664,4 @@ async def get_private_dungeon_image(filename: str, current_user: schemas.User =
610664 if not os .path .exists (user_image_path ):
611665 raise HTTPException (status_code = 404 , detail = "Image not found or you do not have permission to access it." )
612666
613- return FileResponse (user_image_path )
667+ return FileResponse (user_image_path )
0 commit comments