feature: proper multi-tenant-support (#969)(WIP)

* update naming

* refactor tests to use shared structure

* shorten names

* add tools test case

* refactor to support multi-tenant

* set group_id on creation

* initial refactor for multitenant tags/cats

* spelling

* additional test case for same valued resources

* fix recipe update tests

* apply indexes to foreign keys

* fix performance regressions

* handle unknown exception

* utility decorator for function debugging

* migrate recipe_id to UUID

* GUID for recipes

* remove unused import

* move image functions into package

* move utilities to packages dir

* update import

* linter

* image image and asset routes

* update assets and images to use UUIDs

* fix migration base

* image asset test coverage

* use ids for categories and tag crud functions

* refactor recipe organizer test suite to reduce duplication

* add uuid serlization utility

* organizer base router

* slug routes testing and fixes

* fix postgres error

* adopt UUIDs

* move tags, categories, and tools under "organizers" umbrella

* update composite label

* generate ts types

* fix import error

* update frontend types

* fix type errors

* fix postgres errors

* fix #978

* add null check for title validation

* add note in docs on multi-tenancy
This commit is contained in:
Hayden
2022-02-13 12:23:42 -09:00
committed by GitHub
parent 9a82a172cb
commit c617251f4c
157 changed files with 1866 additions and 1578 deletions

View File

@@ -0,0 +1,108 @@
import shutil
from pathlib import Path
import requests
from pydantic import UUID4
from mealie.pkgs import img
from mealie.schema.recipe.recipe import Recipe
from mealie.services._base_service import BaseService
_FIREFOX_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"
class RecipeDataService(BaseService):
minifier: img.ABCMinifier
def __init__(self, recipe_id: UUID4, group_id: UUID4 = None) -> None:
"""
RecipeDataService is a service that consolidates the reading/writing actions related
to assets, and images for a recipe.
"""
super().__init__()
self.recipe_id = recipe_id
self.slug = group_id
self.minifier = img.PillowMinifier(purge=True, logger=self.logger)
self.dir_data = Recipe.directory_from_id(self.recipe_id)
self.dir_image = self.dir_data.joinpath("images")
self.dir_assets = self.dir_data.joinpath("assets")
self.dir_image.mkdir(parents=True, exist_ok=True)
self.dir_assets.mkdir(parents=True, exist_ok=True)
def delete_all_data(self) -> None:
try:
shutil.rmtree(self.dir_data)
except Exception as e:
self.logger.exception(f"Failed to delete recipe data: {e}")
def write_image(self, file_data: bytes, extension: str) -> Path:
extension = extension.replace(".", "")
image_path = self.dir_image.joinpath(f"original.{extension}")
image_path.unlink(missing_ok=True)
if isinstance(file_data, Path):
shutil.copy2(file_data, image_path)
elif isinstance(file_data, bytes):
with open(image_path, "ab") as f:
f.write(file_data)
else:
with open(image_path, "ab") as f:
shutil.copyfileobj(file_data, f)
self.minifier.minify(image_path)
return image_path
def scrape_image(self, image_url) -> None:
self.logger.info(f"Image URL: {image_url}")
if isinstance(image_url, str): # Handles String Types
pass
elif isinstance(image_url, list): # Handles List Types
# Multiple images have been defined in the schema - usually different resolutions
# Typically would be in smallest->biggest order, but can't be certain so test each.
# 'Google will pick the best image to display in Search results based on the aspect ratio and resolution.'
all_image_requests = []
for url in image_url:
if isinstance(url, dict):
url = url.get("url", "")
try:
r = requests.get(url, stream=True, headers={"User-Agent": _FIREFOX_UA})
except Exception:
self.logger.exception("Image {url} could not be requested")
continue
if r.status_code == 200:
all_image_requests.append((url, r))
image_url, _ = max(all_image_requests, key=lambda url_r: len(url_r[1].content), default=("", 0))
elif isinstance(image_url, dict): # Handles Dictionary Types
for key in image_url:
if key == "url":
image_url = image_url.get("url")
ext = image_url.split(".")[-1]
if ext not in img.IMAGE_EXTENSIONS:
ext = "jpg" # Guess the extension
filename = str(self.recipe_id) + "." + ext
filename = Recipe.directory_from_id(self.recipe_id).joinpath("images", filename)
try:
r = requests.get(image_url, stream=True, headers={"User-Agent": _FIREFOX_UA})
except Exception:
self.logger.exception("Fatal Image Request Exception")
return None
if r.status_code == 200:
r.raw.decode_content = True
self.logger.info(f"File Name Suffix {filename.suffix}")
self.write_image(r.raw, filename.suffix)
filename.unlink(missing_ok=True)

View File

@@ -15,7 +15,7 @@ from mealie.schema.recipe.recipe_settings import RecipeSettings
from mealie.schema.recipe.recipe_step import RecipeStep
from mealie.schema.user.user import GroupInDB, PrivateUser
from mealie.services._base_service import BaseService
from mealie.services.image.image import write_image
from mealie.services.recipe.recipe_data_service import RecipeDataService
from .template_service import TemplateService
@@ -142,7 +142,8 @@ class RecipeService(BaseService):
recipe = self.create_one(Recipe(**recipe_dict))
if recipe:
write_image(recipe.slug, recipe_image, "webp")
data_service = RecipeDataService(recipe.id)
data_service.write_image(recipe_image, "webp")
return recipe