feature/finish-recipe-assets (#384)

* add features to readme

* Copy markdown reference

* prop as whole recipe

* parameter as url instead of query

* add card styling to editor

* move images to /recipes/{slug}/images

* add image to breaking changes

* fix delete and import errors

* fix debug/about response

* logger updates

* dashboard ui

* add server side events

* unorganized routes

* default slot

* add backup viewer to dashboard

* format

* add dialog to backup imports

* initial event support

* delete assets when removed

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-05-03 19:32:37 -08:00
committed by GitHub
parent f2d2b79a57
commit 5580d177c3
61 changed files with 1276 additions and 266 deletions

View File

@@ -9,6 +9,7 @@ from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.services.events import create_backup_event
from pathvalidate import sanitize_filename
from pydantic.main import BaseModel
@@ -32,7 +33,7 @@ class ExportDatabase:
export_tag = datetime.now().strftime("%Y-%b-%d")
self.main_dir = app_dirs.TEMP_DIR.joinpath(export_tag)
self.img_dir = self.main_dir.joinpath("images")
self.recipes = self.main_dir.joinpath("recipes")
self.templates_dir = self.main_dir.joinpath("templates")
try:
@@ -43,7 +44,7 @@ class ExportDatabase:
required_dirs = [
self.main_dir,
self.img_dir,
self.recipes,
self.templates_dir,
]
@@ -67,10 +68,10 @@ class ExportDatabase:
with open(out_file, "w") as f:
f.write(content)
def export_images(self):
shutil.copytree(app_dirs.IMG_DIR, self.img_dir, dirs_exist_ok=True)
def export_recipe_dirs(self):
shutil.copytree(app_dirs.RECIPE_DATA_DIR, self.recipes, dirs_exist_ok=True)
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True, slug_folder=False):
items = [x.dict() for x in items]
out_dir = self.main_dir.joinpath(folder_name)
out_dir.mkdir(parents=True, exist_ok=True)
@@ -79,8 +80,10 @@ class ExportDatabase:
ExportDatabase._write_json_file(items, out_dir.joinpath(f"{folder_name}.json"))
else:
for item in items:
filename = sanitize_filename(f"{item.get('name')}.json")
ExportDatabase._write_json_file(item, out_dir.joinpath(filename))
final_dest = out_dir if not slug_folder else out_dir.joinpath(item.get("slug"))
final_dest.mkdir(exist_ok=True)
filename = sanitize_filename(f"{item.get('slug')}.json")
ExportDatabase._write_json_file(item, final_dest.joinpath(filename))
@staticmethod
def _write_json_file(data: Union[dict, list], out_file: Path):
@@ -121,9 +124,9 @@ def backup_all(
if export_recipes:
all_recipes = db.recipes.get_all(session)
db_export.export_items(all_recipes, "recipes", export_list=False)
db_export.export_recipe_dirs()
db_export.export_items(all_recipes, "recipes", export_list=False, slug_folder=True)
db_export.export_templates(all_recipes)
db_export.export_images()
if export_settings:
all_settings = db.settings.get_all(session)
@@ -148,3 +151,5 @@ def auto_backup_job():
session = create_session()
backup_all(session=session, tag="Auto", templates=templates)
logger.info("Auto Backup Called")
create_backup_event("Automated Backup", "Automated backup created", session)
session.close()

View File

@@ -2,7 +2,7 @@ import json
import shutil
import zipfile
from pathlib import Path
from typing import Callable, List
from typing import Callable
from mealie.core.config import app_dirs
from mealie.db.database import db
@@ -49,7 +49,7 @@ class ImportDatabase:
def import_recipes(self):
recipe_dir: Path = self.import_dir.joinpath("recipes")
imports = []
successful_imports = []
successful_imports = {}
recipes = ImportDatabase.read_models_file(
file_path=recipe_dir, model=Recipe, single_file=False, migrate=ImportDatabase._recipe_migration
@@ -68,7 +68,7 @@ class ImportDatabase:
)
if import_status.status:
successful_imports.append(recipe.slug)
successful_imports.update({recipe.slug: recipe})
imports.append(import_status)
@@ -105,15 +105,25 @@ class ImportDatabase:
return recipe_dict
def _import_images(self, successful_imports: List[str]):
def _import_images(self, successful_imports: list[Recipe]):
image_dir = self.import_dir.joinpath("images")
for image in image_dir.iterdir():
if image.stem in successful_imports:
if image.is_dir():
dest = app_dirs.IMG_DIR.joinpath(image.stem)
shutil.copytree(image, dest, dirs_exist_ok=True)
if image.is_file():
shutil.copy(image, app_dirs.IMG_DIR)
if image_dir.exists(): # Migrate from before v0.5.0
for image in image_dir.iterdir():
item: Recipe = successful_imports.get(image.stem)
if item:
dest_dir = item.image_dir
if image.is_dir():
shutil.copytree(image, dest_dir, dirs_exist_ok=True)
if image.is_file():
shutil.copy(image, dest_dir)
else:
recipe_dir = self.import_dir.joinpath("recipes")
shutil.copytree(recipe_dir, app_dirs.RECIPE_DATA_DIR, dirs_exist_ok=True)
minify.migrate_images()
@@ -227,7 +237,7 @@ class ImportDatabase:
return [model(**g) for g in file_data]
all_models = []
for file in file_path.glob("*.json"):
for file in file_path.glob("**/*.json"):
with open(file, "r") as f:
file_data = json.loads(f.read())

40
mealie/services/events.py Normal file
View File

@@ -0,0 +1,40 @@
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.events import Event, EventCategory
from sqlalchemy.orm.session import Session
def save_event(title, text, category, session: Session):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db.events.create(session, event.dict())
def create_general_event(title, text, session=None):
category = EventCategory.general
save_event(title=title, text=text, category=category, session=session)
def create_recipe_event(title, text, session=None):
category = EventCategory.recipe
save_event(title=title, text=text, category=category, session=session)
def create_backup_event(title, text, session=None):
category = EventCategory.backup
save_event(title=title, text=text, category=category, session=session)
def create_scheduled_event(title, text, session=None):
category = EventCategory.scheduled
save_event(title=title, text=text, category=category, session=session)
def create_migration_event(title, text, session=None):
category = EventCategory.migration
save_event(title=title, text=text, category=category, session=session)
def create_sign_up_event(title, text, session=None):
category = EventCategory.sign_up
save_event(title=title, text=text, category=category, session=session)

View File

@@ -4,7 +4,7 @@ from pathlib import Path
import requests
from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.schema.recipe import Recipe
from mealie.services.image import minify
logger = root_logger.get_logger()
@@ -20,47 +20,11 @@ class ImageOptions:
IMG_OPTIONS = ImageOptions()
def read_image(recipe_slug: str, image_type: str = "original") -> Path:
"""returns the path to the image file for the recipe base of image_type
Args:
recipe_slug (str): Recipe Slug
image_type (str, optional): Glob Style Matcher "original*" | "min-original* | "tiny-original*"
Returns:
Path: [description]
"""
recipe_slug = recipe_slug.split(".")[0] # Incase of File Name
recipe_image_dir = app_dirs.IMG_DIR.joinpath(recipe_slug)
for file in recipe_image_dir.glob(image_type):
return file
return None
def rename_image(original_slug, new_slug) -> Path:
current_path = app_dirs.IMG_DIR.joinpath(original_slug)
new_path = app_dirs.IMG_DIR.joinpath(new_slug)
try:
new_path = current_path.rename(new_path)
except FileNotFoundError:
logger.error(f"Image Directory {original_slug} Doesn't Exist")
return new_path
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path:
try:
delete_image(recipe_slug)
except Exception:
pass
image_dir = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}"))
image_dir.mkdir(exist_ok=True, parents=True)
image_dir = Recipe(slug=recipe_slug).image_dir
extension = extension.replace(".", "")
image_path = image_dir.joinpath(f"original.{extension}")
image_path.unlink(missing_ok=True)
if isinstance(file_data, Path):
shutil.copy2(file_data, image_path)
@@ -77,12 +41,6 @@ def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path:
return image_path
def delete_image(recipe_slug: str) -> str:
recipe_slug = recipe_slug.split(".")[0]
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return shutil.rmtree(file)
def scrape_image(image_url: str, slug: str) -> Path:
if isinstance(image_url, str): # Handles String Types
image_url = image_url
@@ -96,7 +54,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
image_url = image_url.get("url")
filename = slug + "." + image_url.split(".")[-1]
filename = app_dirs.IMG_DIR.joinpath(filename)
filename = Recipe(slug=slug).image_dir.joinpath(filename)
try:
r = requests.get(image_url, stream=True)

View File

@@ -4,10 +4,8 @@ from pathlib import Path
from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.recipe import Recipe
from PIL import Image
from sqlalchemy.orm.session import Session
logger = root_logger.get_logger()
@@ -20,11 +18,7 @@ class ImageSizes:
def get_image_sizes(org_img: Path, min_img: Path, tiny_img: Path) -> ImageSizes:
return ImageSizes(
org=sizeof_fmt(org_img),
min=sizeof_fmt(min_img),
tiny=sizeof_fmt(tiny_img),
)
return ImageSizes(org=sizeof_fmt(org_img), min=sizeof_fmt(min_img), tiny=sizeof_fmt(tiny_img))
def minify_image(image_file: Path) -> ImageSizes:
@@ -110,28 +104,9 @@ def move_all_images():
if new_file.is_file():
new_file.unlink()
image_file.rename(new_file)
def validate_slugs_in_database(session: Session = None):
def check_image_path(image_name: str, slug_path: str) -> bool:
existing_path: Path = app_dirs.IMG_DIR.joinpath(image_name)
slug_path: Path = app_dirs.IMG_DIR.joinpath(slug_path)
if existing_path.is_dir():
slug_path.rename(existing_path)
else:
logger.info("No Image Found")
session = session or create_session()
all_recipes = db.recipes.get_all(session)
slugs_and_images = [(x.slug, x.image) for x in all_recipes]
for slug, image in slugs_and_images:
image_slug = image.split(".")[0] # Remove Extension
if slug != image_slug:
logger.info(f"{slug}, Doesn't Match '{image_slug}'")
check_image_path(image, slug)
if image_file.is_dir():
slug = image_file.name
image_file.rename(Recipe(slug=slug).image_dir)
def migrate_images():
@@ -139,7 +114,7 @@ def migrate_images():
move_all_images()
for image in app_dirs.IMG_DIR.glob("*/original.*"):
for image in app_dirs.RECIPE_DATA_DIR.glob("**/original.*"):
minify_image(image)
@@ -148,4 +123,3 @@ def migrate_images():
if __name__ == "__main__":
migrate_images()
validate_slugs_in_database()

View File

View File

@@ -0,0 +1,34 @@
from pathlib import Path
from shutil import copytree, rmtree
from mealie.core.config import app_dirs
from mealie.core.root_logger import get_logger
from mealie.schema.recipe import Recipe
logger = get_logger()
def check_assets(original_slug, recipe: Recipe) -> None:
if original_slug != recipe.slug:
current_dir = app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
try:
copytree(current_dir, recipe.directory, dirs_exist_ok=True)
except FileNotFoundError:
logger.error(f"Recipe Directory not Found: {original_slug}")
logger.info(f"Renaming Recipe Directory: {original_slug} -> {recipe.slug}")
all_asset_files = [x.file_name for x in recipe.assets]
for file in recipe.asset_dir.iterdir():
file: Path
if file.is_dir():
continue
if file.name not in all_asset_files:
file.unlink()
def delete_assets(recipe_slug):
recipe_dir = Recipe(slug=recipe_slug).directory
rmtree(recipe_dir, ignore_errors=True)
logger.info(f"Recipe Directory Removed: {recipe_slug}")