Feature/user photo storage (#877)

* add default assets for user profile

* add recipe avatar

* change user_id to UUID

* add profile image upload

* setup image cache keys

* cleanup tests and add image tests

* purge user data on delete

* new user repository tests

* add user_id validator for int -> UUID conversion

* delete depreciated route

* force set content type

* refactor tests to use temp directory

* validate parent exists before createing

* set user_id to correct type

* update instruction id

* reset primary key on migration
This commit is contained in:
Hayden
2021-12-18 19:04:36 -09:00
committed by GitHub
parent a2f8f27193
commit ea7c4771ee
64 changed files with 433 additions and 181 deletions

View File

@@ -1,7 +1,8 @@
from fastapi import APIRouter
from . import recipe
from . import media_recipe, media_user
media_router = APIRouter(prefix="/api/media", tags=["Recipe: Images and Assets"])
media_router.include_router(recipe.router)
media_router.include_router(media_recipe.router)
media_router.include_router(media_user.router)

View File

@@ -0,0 +1,24 @@
from fastapi import APIRouter, HTTPException, status
from pydantic import UUID4
from starlette.responses import FileResponse
from mealie.schema.user import PrivateUser
"""
These routes are for development only! These assets are served by Caddy when not
in development mode. If you make changes, be sure to test the production container.
"""
router = APIRouter(prefix="/users")
@router.get("/{user_id}/{file_name}", response_class=FileResponse)
async def get_user_image(user_id: UUID4, file_name: str):
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
and should not hit the API in production"""
recipe_image = PrivateUser.get_directory(user_id) / file_name
if recipe_image.exists():
return FileResponse(recipe_image, media_type="image/webp")
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)

View File

@@ -22,7 +22,7 @@ async def create_api_token(
):
"""Create api_token in the Database"""
token_data = {"long_token": True, "id": current_user.id}
token_data = {"long_token": True, "id": str(current_user.id)}
five_years = timedelta(1825)
token = create_access_token(token_data, five_years)
@@ -30,7 +30,7 @@ async def create_api_token(
token_model = CreateToken(
name=token_name.name,
token=token,
parent_id=current_user.id,
user_id=current_user.id,
)
db = get_database(session)

View File

@@ -1,4 +1,5 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status
from pydantic import UUID4
from sqlalchemy.orm.session import Session
from mealie.core import security
@@ -39,15 +40,15 @@ async def create_user(
@admin_router.get("/{id}", response_model=UserOut)
async def get_user(id: int, session: Session = Depends(generate_session)):
async def get_user(id: UUID4, session: Session = Depends(generate_session)):
db = get_database(session)
return db.users.get(id)
@admin_router.delete("/{id}")
def delete_user(
id: UUID4,
background_tasks: BackgroundTasks,
id: int,
session: Session = Depends(generate_session),
current_user: PrivateUser = Depends(get_current_user),
):
@@ -55,7 +56,7 @@ def delete_user(
assert_user_change_allowed(id, current_user)
if id == 1:
if id == 1: # TODO: identify super_user
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="SUPER_USER")
try:
@@ -75,7 +76,7 @@ async def get_logged_in_user(
@user_router.put("/{id}")
async def update_user(
id: int,
id: UUID4,
new_data: UserBase,
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),

View File

@@ -1,51 +1,49 @@
import shutil
from pathlib import Path
from fastapi import Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from fastapi.routing import APIRouter
from pydantic import UUID4
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs
app_dirs = get_app_dirs()
from mealie import utils
from mealie.core.dependencies import get_current_user
from mealie.core.dependencies.dependencies import temporary_dir
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import PrivateUser
from mealie.services.image import minify
public_router = APIRouter(prefix="", tags=["Users: Images"])
user_router = UserAPIRouter(prefix="", tags=["Users: Images"])
@public_router.get("/{id}/image")
async def get_user_image(id: str):
"""Returns a users profile picture"""
user_dir = app_dirs.USER_DIR.joinpath(id)
for recipe_image in user_dir.glob("profile_image.*"):
return FileResponse(recipe_image)
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)
@user_router.post("/{id}/image")
def update_user_image(
id: str,
profile_image: UploadFile = File(...),
id: UUID4,
profile: UploadFile = File(...),
temp_dir: Path = Depends(temporary_dir),
current_user: PrivateUser = Depends(get_current_user),
session: Session = Depends(generate_session),
):
"""Updates a User Image"""
assert_user_change_allowed(id, current_user)
extension = profile_image.filename.split(".")[-1]
temp_img = temp_dir.joinpath(profile.filename)
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
with temp_img.open("wb") as buffer:
shutil.copyfileobj(profile.file, buffer)
[x.unlink() for x in app_dirs.USER_DIR.joinpath(id).glob("profile_image.*")]
image = minify.to_webp(temp_img)
dest = PrivateUser.get_directory(id) / "profile.webp"
dest = app_dirs.USER_DIR.joinpath(id, f"profile_image.{extension}")
shutil.copyfile(image, dest)
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
db = get_database(session)
db.users.patch(id, {"cache_key": utils.new_cache_key()})
if not dest.is_file:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)