diff --git a/app/api/api_v2/api.py b/app/api/api_v2/api.py index 5894c88..36c8411 100644 --- a/app/api/api_v2/api.py +++ b/app/api/api_v2/api.py @@ -1,8 +1,9 @@ from fastapi import APIRouter -from app.api.api_v2.endpoints import delete, list_videos, upload +from app.api.api_v2.endpoints import delete, list_videos, patch_video, upload api_router = APIRouter() api_router.include_router(list_videos.router, tags=['files']) api_router.include_router(upload.router, tags=['files']) api_router.include_router(delete.router, tags=['files']) +api_router.include_router(patch_video.router, tags=['files']) diff --git a/app/api/api_v2/endpoints/patch_video.py b/app/api/api_v2/endpoints/patch_video.py new file mode 100644 index 0000000..8f1706c --- /dev/null +++ b/app/api/api_v2/endpoints/patch_video.py @@ -0,0 +1,77 @@ +from typing import Any, Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, Field +from sqlalchemy import and_ +from sqlalchemy.orm import selectinload +from sqlmodel import select +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.api.dependencies import yield_db_session +from app.api.security import cognito_signed_in +from app.models.klepp import Tag, TagBase, User, Video, VideoRead + +router = APIRouter() + + +class VideoPatch(BaseModel): + path: str + display_name: Optional[str] = Field(default=None, regex=r'^[\s\w\d_-]*$', min_length=2, max_length=40) + hidden: bool = Field(default=False) + tags: list[TagBase] = Field(default=[]) + + +@router.patch('/files', response_model=VideoRead) +async def get_all_files( + video_patch: VideoPatch, + db_session: AsyncSession = Depends(yield_db_session), + user: User = Depends(cognito_signed_in), +) -> Any: + """ + Patch + """ + excluded = video_patch.dict(exclude_unset=True) + query_video = ( + select(Video) + .where(and_(Video.path == video_patch.path, Video.user_id == user.id)) + .options(selectinload(Video.tags)) + ) + db_result = await db_session.exec(query_video) # type: ignore + video = db_result.first() + if not video: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='File not found. Ensure you own the file, and that the file already exist.', + ) + if excluded_tags := excluded.get('tags'): + # They want to update tags, fetch available tags first + list_tag = [tag['name'] for tag in excluded_tags] + query_tags = select(Tag).where(Tag.name.in_(list_tag)) # type: ignore + tag_result = await db_session.exec(query_tags) # type: ignore + tags: list[Tag] = tag_result.all() + if len(list_tag) != len(tags): + db_list_tag = [tag.name for tag in tags] + not_found_tags = [tag for tag in list_tag if tag not in db_list_tag] + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Tag {", ".join(not_found_tags)} not found.', + ) + video.tags = tags + excluded.pop('tags') + + # Patch remaining attributes + for key, value in excluded.items(): + setattr(video, key, value) + + db_session.add(video) + await db_session.commit() + # To keep responses equal between list and post APIs, we fetch it all + query_video = ( + select(Video) + .where(Video.path == video_patch.path) + .options(selectinload(Video.user)) + .options(selectinload(Video.tags)) + .options(selectinload(Video.likes)) + ) + result = await db_session.exec(query_video) # type: ignore + return result.first() diff --git a/app/api/api_v2/endpoints/upload.py b/app/api/api_v2/endpoints/upload.py index 62753e0..996f7a5 100644 --- a/app/api/api_v2/endpoints/upload.py +++ b/app/api/api_v2/endpoints/upload.py @@ -36,7 +36,7 @@ async def upload_video(boto_session: AioBaseClient, path: str, temp_video_name: async def upload_file( file: UploadFile = File(..., description='File to upload'), file_name: Optional[str] = Form( - default=None, example='my_file.mp4', regex=r'^[\s\w\d_-]*$', min_length=2, max_length=40 + default=None, example='my_file', regex=r'^[\s\w\d_-]*$', min_length=2, max_length=40 ), boto_session: AioBaseClient = Depends(get_boto), user: User = Depends(cognito_signed_in),