# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from datetime import datetime, timedelta from fastapi import APIRouter, HTTPException, UploadFile, File, Depends from fastapi.responses import StreamingResponse, RedirectResponse from pydantic import BaseModel from classquiz.auth import get_current_user from classquiz.config import settings, storage, arq from classquiz.db.models import User, StorageItem, PublicStorageItem, UpdateStorageItem, PrivateStorageItem from classquiz.helpers import check_image_string from classquiz.storage.errors import DownloadingFailedError from uuid import uuid4, UUID settings = settings() router = APIRouter() @router.get("/download/{file_name}") async def download_file(file_name: str): checked_image_string = check_image_string(file_name) if not checked_image_string[0]: raise HTTPException(status_code=400, detail="Invalid file name") if checked_image_string[1] is not None: item = await StorageItem.objects.get_or_none(id=checked_image_string[1]) if item is None: raise HTTPException(status_code=404, detail="File not found") file_name = item.storage_path if file_name is None: file_name = item.id.hex if storage.backend == "s3": return RedirectResponse(url=await storage.get_url(file_name, 300)) try: download = await storage.download(file_name) except DownloadingFailedError: raise HTTPException(status_code=404, detail="File not found") if download is None: raise HTTPException(status_code=404, detail="File not found") def iter_file(): yield from download return StreamingResponse( iter_file(), media_type="image/*", headers={"Cache-Control": "public, immutable, max-age=31536000"}, ) @router.post("/") async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)) -> PublicStorageItem: if user.storage_used > settings.free_storage_limit: raise HTTPException(status_code=409, detail="Storage limit reached") file_id = uuid4() size = 0 file_obj = StorageItem( id=file_id, uploaded_at=datetime.now(), mime_type=file.content_type, hash=None, user=user, size=size, deleted_at=None, alt_text=None, ) file_data = await file.read() await storage.upload(file_name=file_id.hex, file_data=file_data) await file_obj.save() await arq.enqueue_job("calculate_hash", file_id.hex) return PublicStorageItem.from_db_model(file_obj) @router.get("/meta/{file_id}") async def get_file_info(file_id: UUID, user: User = Depends(get_current_user)) -> PublicStorageItem: file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None) if file_data is None: raise HTTPException(status_code=404, detail="File not found") return PublicStorageItem.from_db_model(file_data) @router.delete("/meta/{file_id}") async def mark_file_as_deleted(file_id: UUID, user: User = Depends(get_current_user)): file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None) if file_data is None: raise HTTPException(status_code=404, detail="File not found") storage_path = file_data.storage_path if storage_path is None: storage_path = file_data.id.hex await storage.delete(storage_path) file_data.deleted_at = datetime.now() await file_data.update() return @router.put("/meta/{file_id}") async def update_image_data( file_id: UUID, data: UpdateStorageItem, user: User = Depends(get_current_user) ) -> PublicStorageItem: file_data = await StorageItem.objects.get_or_none(id=file_id, user=user, deleted_at=None) if file_data is None: raise HTTPException(status_code=404, detail="File not found") file_data.filename = data.filename file_data.alt_text = data.alt_text await file_data.update() return PublicStorageItem.from_db_model(file_data) @router.get("/list") async def list_images( since: datetime | None = None, user: User = Depends(get_current_user) ) -> list[PrivateStorageItem]: if since is None: since = datetime.now() - timedelta(days=2) storage_items = ( await StorageItem.objects.filter(user=user) .filter(StorageItem.uploaded_at > since) .order_by(StorageItem.uploaded_at.desc()) .select_related([StorageItem.quizzes, StorageItem.quiztivities]) .all() ) if len(storage_items) == 0: raise HTTPException(status_code=404, detail="No items found") return_items: list[PrivateStorageItem] = [] for item in storage_items: # print(item.quizzes) return_items.append(PrivateStorageItem.from_db_model(item)) return return_items class ReturnGetStorageLimit(BaseModel): limit: int limit_reached: bool used: int @router.get("/limit") async def get_storage_limit(user: User = Depends(get_current_user)) -> ReturnGetStorageLimit: user = await User.objects.get_or_none(id=user.id) if user.storage_used > settings.free_storage_limit: return ReturnGetStorageLimit(limit=settings.free_storage_limit, limit_reached=True, used=user.storage_used) else: return ReturnGetStorageLimit(limit=settings.free_storage_limit, limit_reached=False, used=user.storage_used)