move images to /recipes/{slug}/images

This commit is contained in:
hay-kot 2021-05-02 14:22:00 -08:00
commit 0a6a29a004
15 changed files with 121 additions and 128 deletions

View file

@ -10,8 +10,8 @@
encode gzip
uri strip_suffix /
handle_path /api/recipes/image/* {
root * /app/data/img/
handle_path /api/recipes/media/* {
root * /app/data/recipes/
file_server
}

View file

@ -14,9 +14,9 @@ const recipeURLs = {
recipe: slug => prefix + slug,
update: slug => prefix + slug,
delete: slug => prefix + slug,
createAsset: slug => `${prefix}media/${slug}/assets`,
recipeImage: slug => `${prefix}${slug}/image`,
updateImage: slug => `${prefix}${slug}/image`,
createAsset: slug => `${prefix}${slug}/asset`,
};
export const recipeAPI = {
@ -84,7 +84,7 @@ export const recipeAPI = {
fd.append("extension", fileObject.name.split(".").pop());
fd.append("name", name);
fd.append("icon", icon);
let response = apiReq.post(recipeURLs.createAsset(recipeSlug), fd);
const response = apiReq.post(recipeURLs.createAsset(recipeSlug), fd);
return response;
},
@ -135,14 +135,14 @@ export const recipeAPI = {
},
recipeImage(recipeSlug) {
return `/api/recipes/image/${recipeSlug}/original.webp`;
return `/api/recipes/media/${recipeSlug}/image/original.webp`;
},
recipeSmallImage(recipeSlug) {
return `/api/recipes/image/${recipeSlug}/min-original.webp`;
return `/api/recipes/media/${recipeSlug}/image/min-original.webp`;
},
recipeTinyImage(recipeSlug) {
return `/api/recipes/image/${recipeSlug}/tiny-original.webp`;
return `/api/recipes/media/${recipeSlug}/image/tiny-original.webp`;
},
};

View file

@ -18,7 +18,7 @@
v-if="!edit"
color="primary"
icon
:href="`/api/recipes/${slug}/asset?file_name=${item.fileName}`"
:href="`/api/recipes/media/${slug}/assets/${item.fileName}`"
target="_blank"
top
>
@ -135,7 +135,7 @@ export default {
this.value.splice(index, 1);
},
copyLink(name, fileName) {
const copyText = `![${name}](${this.baseURL}/api/recipes/${this.slug}/assets/${fileName})`;
const copyText = `![${name}](${this.baseURL}/api/recipes/media/${this.slug}/assets/${fileName})`;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)

View file

@ -3,6 +3,7 @@ import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from mealie.core.config import app_dirs
from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
@ -12,6 +13,7 @@ from mealie.services.backups.exports import backup_all
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
logger = get_logger()
@router.get("/available", response_model=Imports)
@ -44,7 +46,8 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
export_groups=data.options.groups,
)
return {"export_path": export_path}
except Exception:
except Exception as e:
logger.error(e)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)

View file

@ -88,7 +88,7 @@ def get_todays_image(session: Session = Depends(generate_session), group_name: s
recipe = get_todays_meal(session, group_in_db)
if recipe:
recipe_image = image.read_image(recipe.slug, image_type=image.IMG_OPTIONS.ORIGINAL_IMAGE)
recipe_image = recipe.image_dir.joinpath(image.ImageOptions.ORIGINAL_IMAGE)
else:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if recipe_image:

View file

@ -1,10 +1,10 @@
from fastapi import APIRouter
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_assets, recipe_crud_routes, tag_routes
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes
router = APIRouter()
router.include_router(all_recipe_routes.router)
router.include_router(recipe_crud_routes.router)
router.include_router(recipe_assets.router)
router.include_router(recipe_media.router)
router.include_router(category_routes.router)
router.include_router(tag_routes.router)

View file

@ -4,7 +4,8 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.recipe import Recipe, RecipeURLIn
from mealie.services.image.image import delete_image, rename_image, scrape_image, write_image
from mealie.services.image.image import scrape_image, write_image
from mealie.services.recipe.asset import check_asset
from mealie.services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session
@ -58,7 +59,7 @@ def update_recipe(
print(recipe.assets)
if recipe_slug != recipe.slug:
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
check_asset(original_slug=recipe_slug, recipe=recipe)
return recipe
@ -76,7 +77,7 @@ def patch_recipe(
session, recipe_slug, new_data=data.dict(exclude_unset=True, exclude_defaults=True)
)
if recipe_slug != recipe.slug:
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
check_asset(original_slug=recipe_slug, recipe=recipe)
return recipe
@ -91,7 +92,6 @@ def delete_recipe(
try:
delete_data = db.recipes.delete(session, recipe_slug)
delete_image(recipe_slug)
return delete_data
except Exception:

View file

@ -3,7 +3,6 @@ from enum import Enum
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
from fastapi.datastructures import UploadFile
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
@ -12,7 +11,7 @@ from slugify import slugify
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
router = APIRouter(prefix="/api/recipes", tags=["Recipe Media"])
router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"])
class ImageType(str, Enum):
@ -21,11 +20,12 @@ class ImageType(str, Enum):
tiny = "tiny-original.webp"
@router.get("/image/{recipe_slug}/{file_name}")
@router.get("/{recipe_slug}/image/{file_name}")
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
"""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 = app_dirs.IMG_DIR.joinpath(recipe_slug, file_name.value)
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
if recipe_image:
return FileResponse(recipe_image)
else:
@ -33,13 +33,17 @@ async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.orig
@router.get("/{recipe_slug}/assets/{file_name}")
async def get_recipe_asset(recipe_slug, file_name: str):
async def get_recipe_asset(recipe_slug: str, file_name: str):
""" Returns a recipe asset """
file = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
try:
return FileResponse(file)
except Exception:
raise HTTPException(status.HTTP_404_NOT_FOUND)
@router.post("/{recipe_slug}/asset", response_model=RecipeAsset)
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
name: str = Form(...),
@ -52,8 +56,7 @@ def upload_recipe_asset(
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
dest = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
dest.parent.mkdir(exist_ok=True, parents=True)
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
with dest.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)

View file

@ -1,7 +1,9 @@
import datetime
from pathlib import Path
from typing import Any, Optional
from fastapi_camelcase import CamelModel
from mealie.core.config import app_dirs
from mealie.db.models.recipe.recipe import RecipeModel
from pydantic import BaseModel, Field, validator
from pydantic.utils import GetterDict
@ -58,8 +60,8 @@ class Nutrition(CamelModel):
class RecipeSummary(CamelModel):
id: Optional[int]
name: str
slug: Optional[str] = ""
name: Optional[str]
slug: str = ""
image: Optional[Any]
description: Optional[str]
@ -98,6 +100,28 @@ class Recipe(RecipeSummary):
org_url: Optional[str] = Field(None, alias="orgURL")
extras: Optional[dict] = {}
@staticmethod
def directory_from_slug(slug) -> Path:
return app_dirs.RECIPE_DATA_DIR.joinpath(slug)
@property
def directory(self) -> Path:
dir = app_dirs.RECIPE_DATA_DIR.joinpath(self.slug)
dir.mkdir(exist_ok=True, parents=True)
return dir
@property
def asset_dir(self) -> Path:
dir = self.directory.joinpath("assets")
dir.mkdir(exist_ok=True, parents=True)
return dir
@property
def image_dir(self) -> Path:
dir = self.directory.joinpath("images")
dir.mkdir(exist_ok=True, parents=True)
return dir
class Config:
orm_mode = True
@ -140,6 +164,8 @@ class Recipe(RecipeSummary):
@validator("slug", always=True, pre=True)
def validate_slug(slug: str, values):
if not values["name"]:
return slug
name: str = values["name"]
calc_slug: str = slugify(name)

View file

@ -32,7 +32,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 +43,7 @@ class ExportDatabase:
required_dirs = [
self.main_dir,
self.img_dir,
self.recipes,
self.templates_dir,
]
@ -67,10 +67,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 +79,9 @@ 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"))
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 +122,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)

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,21 @@ 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")
if image_dir.exists():
for image in image_dir.iterdir():
if image.stem in successful_imports:
item: Recipe = successful_imports.get(image.stem)
if item:
dest_dir = item.image_dir
if image.is_dir():
dest = app_dirs.IMG_DIR.joinpath(image.stem)
shutil.copytree(image, dest, dirs_exist_ok=True)
shutil.copytree(image, dest_dir, dirs_exist_ok=True)
if image.is_file():
shutil.copy(image, app_dirs.IMG_DIR)
shutil.copy(image, dest_dir)
minify.migrate_images()
@ -227,7 +233,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())

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,22 @@
from pathlib import Path
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_asset(original_slug, recipe: Recipe) -> Path:
if original_slug == recipe.slug:
return recipe.assets
current_dir = app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
try:
current_dir.rename(recipe.directory)
except FileNotFoundError:
logger.error(f"Recipe Directory not Found: {original_slug}")
logger.info(f"Renaming Recipe Directory: {original_slug} -> {recipe.slug}")
return current_dir.absolute()