backup improvements

This commit is contained in:
Hayden 2021-01-11 21:58:03 -09:00
commit 400fbac7b9
14 changed files with 249 additions and 173 deletions

View file

@ -121,10 +121,9 @@ export default {
async createBackup() {
this.backupLoading = true;
let response = await api.backups.create(
this.backupTag,
this.selectedTemplate
);
let response = await api.backups.create(this.backupTag, [
this.selectedTemplate,
]);
if (response.status == 201) {
this.selectedBackup = null;

View file

@ -1,29 +1,33 @@
{
"@context": "http://schema.org",
"@type": "Recipe",
"articleBody": "\u201cAfter a draining day juggling work, homeschooling, and urging children to stop using their masks as slingshots, the ideal food for me isn\u2019t perfectly prepared food that\u2019s been tweezered into position, but a meal that\u2019s simply comforting,\u201d writes the Smitten Kitchen\u2019s Deb Perelman. Right now, it\u2019s this deeply cozy pot of tender chicken thighs, jammy leeks, and broth-soaked rice.",
"alternativeHeadline": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"dateModified": "2021-01-10 15:20:51.422000",
"datePublished": "2020-08-18 04:00:00",
"articleBody": "Leftover rice is ideal for this dish (and a great way to use up any takeout that\u2019s hanging around), since fully chilled rice tends to be drier and will become crispier and browner in the skillet. To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it out like a pancake. Don\u2019t touch until you hear it crackle! Finish with a sunny-side-up egg\u2014or poach it if you don't mind the stovetop fuss. This recipe is part of the 2021\u00a0Feel Good Food Plan, our eight-day dinner plan for starting the year off right.",
"alternativeHeadline": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle!",
"dateModified": "2021-01-11 23:25:22.997000",
"datePublished": "2021-01-01 06:00:00",
"keywords": [
"recipes",
"chicken recipes",
"kosher salt",
"black pepper",
"butter",
"leek",
"lemon zest",
"rice",
"chicken broth",
"anchovy",
"garlic",
"capers",
"herb",
"olive oil",
"healthyish",
"salad",
"ginger",
"garlic",
"orange",
"oil",
"soy sauce",
"lemon juice",
"sesame oil",
"kosher salt",
"broccoli",
"brown rice",
"egg",
"celery",
"cilantro",
"mint",
"feel good food plan 2021",
"feel good food plan",
"web"
],
"thumbnailUrl": "https://assets.bonappetit.com/photos/5f29796456f43685a49327fb/1:1/w_1125,h_1125,c_limit/Chicken-and-Rice-With-Leeks-Salsa-Verde-01.jpg",
"thumbnailUrl": "https://assets.bonappetit.com/photos/5fdbe70a84d333dd1dcc7900/1:1/w_1698,h_1698,c_limit/BA1220feelgoodalt.jpg",
"publisher": {
"@context": "https://schema.org",
"@type": "Organization",
@ -47,49 +51,68 @@
"author": [
{
"@type": "Person",
"name": "Deb Perelman",
"sameAs": "https://bon-appetit.com/contributor/deb-perelman/"
"name": "Devonn Francis",
"sameAs": "https://bon-appetit.com/contributor/devonn-francis/"
}
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.02,
"ratingCount": 48
"ratingValue": 4.33,
"ratingCount": 33
},
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
"headline": "Chicken and Rice With Leeks and Salsa Verde",
"name": "Chicken and Rice With Leeks and Salsa Verde",
"description": "To get the best texture, evenly distribute the rice in your pan and gently press down to flatten it. Don\u2019t touch until you hear it crackle! ",
"image": "crispy-rice-with-ginger-citrus-celery-salad.jpg",
"headline": "Crispy Rice With Ginger-Citrus Celery Salad",
"name": "Crispy Rice With Ginger-Citrus Celery Salad",
"recipeIngredient": [
"1\u00bd lb. skinless, boneless chicken thighs (4\u20138 depending on size)",
"Kosher salt, freshly ground pepper",
"3 Tbsp. unsalted butter, divided",
"2 large or 3 medium leeks, white and pale green parts only, halved lengthwise, thinly sliced",
"Zest and juice of 1 lemon, divided",
"1\u00bd cups long-grain white rice, rinsed until water runs clear",
"2\u00be cups low-sodium chicken broth",
"1 oil-packed anchovy fillet",
"2 garlic cloves",
"1 Tbsp. drained capers",
"Crushed red pepper flakes",
"1 cup tender herb leaves (such as parsley, cilantro, and/or mint)",
"4\u20135 Tbsp. extra-virgin olive oil"
"1 2\" piece ginger, peeled, finely grated",
"1 small garlic clove, finely grated",
"Juice of 1 orange",
"2 tbsp. vegetable oil",
"1Tbsp. coconut aminos or low-sodium soy sauce",
"1 Tbsp. fresh lemon juice",
"\u00bc tsp. toasted sesame oil",
"Kosher salt",
"1 medium head of broccoli",
"6 Tbsp. (or more) vegetable oil, divided",
"Kosher salt",
"2 cups chilled cooked brown rice",
"4 large eggs",
"3 celery stalks, thinly sliced on a steep diagonal",
"\u00bd cup cilantro leaves with tender stems",
"\u00bd cup mint leaves",
"Crushed red pepper flakes (for serving)"
],
"recipeInstructions": [
{
"text": "Season chicken with salt and pepper. Melt 2 Tbsp. butter in a large high-sided skillet over medium-high heat. Add leeks and half of lemon zest, season with salt and pepper, and mix to coat leeks in butter. Reduce heat to medium-low, cover, and cook, stirring occasionally, until leeks are somewhat tender, about 5 minutes. Remove lid, increase heat to medium-high, and cook, stirring occasionally, until tender and just starting to take on color, about 3 minutes. Add rice and cook, stirring often, 3 minutes, then add broth, scraping up any browned bits. Tuck short sides of each chicken thigh underneath so they are touching and nestle seam side down into rice mixture. Bring to a simmer. Cover, reduce heat to medium-low, and cook until rice is tender and chicken is cooked through, about 20 minutes. Remove from heat. Cut remaining 1 Tbsp. butter into small pieces and scatter over mixture. Re-cover and let sit 10 minutes."
"text": "Whisk ginger, garlic, orange juice, vegetable oil, coconut aminos, lemon juice, and sesame oil in a small bowl; season with salt and set aside."
},
{
"text": "Meanwhile, pulse anchovy, garlic, capers, a few pinches of red pepper flakes, and remaining lemon zest in a food processor until finely chopped. Add herbs; process until a paste forms. With motor running, gradually stream in oil until loosened to a thick sauce. Add half of lemon juice; season salsa verde with salt."
"text": "Trim about \u00bd\" from woody end of broccoli stem. Peel tough outer layer from stem. Cut florets from stems and thinly slice stems about \u00bd\" thick. Break florets apart with your hands into 1\"\u20131\u00bd\" pieces."
},
{
"text": "Drizzle remaining lemon juice over chicken and rice. Serve with salsa verde."
"text": "Heat 2 Tbsp. oil in a large nonstick skillet over medium. Working in 2 batches if needed, arrange broccoli in a single layer and cook, tossing occasionally, until broccoli is bright green and lightly charred around the edges, about\u00a03 minutes. Transfer to a large plate."
},
{
"text": "Pour 2 Tbsp. oil into same pan and heat over medium-high. Once you see the first wisp of smoke, add rice and season lightly with salt. Using a spatula or spoon, press rice evenly into pan like a pancake. Rice will begin to crackle, but don\u2019t fuss with it. When the crackling has died down almost completely, about\u00a03 minutes, break rice into large pieces and turn over."
},
{
"text": "Add broccoli back to pan and give everything a toss to combine. Cook, tossing occasionally and adding another\u00a01 Tbsp. oil if pan looks dry, until broccoli is tender and rice is warmed through and very crisp, about 5 minutes. Transfer mixture to a platter or divide among plates and set aside."
},
{
"text": "Wipe out skillet; heat remaining\u00a02 Tbsp. oil over medium-high. Crack eggs into skillet; season with salt. Oil should bubble around eggs right away. Cook, rotating skillet occasionally, until whites are golden brown and crisp at the edges and set around the yolk (which should be runny), about 2 minutes."
},
{
"text": "Toss celery, cilantro, and mint with\u00a03 Tbsp. reserved dressing and a pinch of salt in a medium bowl to combine."
},
{
"text": "Scatter celery salad over fried rice; top with fried eggs and sprinkle with red pepper flakes. Serve extra dressing alongside."
}
],
"recipeYield": "4 Servings",
"url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
"orgURL": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
"recipeYield": "4 servings",
"url": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad",
"slug": "crispy-rice-with-ginger-citrus-celery-salad",
"orgURL": "https://www.bonappetit.com/recipe/crispy-rice-with-ginger-citrus-celery-salad",
"categories": [],
"tags": [],
"dateAdded": null,

View file

@ -13,7 +13,9 @@ class BaseDocument:
self.document: mongoengine.Document
@staticmethod
def _unpack_mongo(document) -> dict: # TODO: Probably Put a version in each class to speed up reads?
def _unpack_mongo(
document,
) -> dict: # TODO: Probably Put a version in each class to speed up reads?
document = json.loads(document.to_json())
del document["_id"]
@ -41,19 +43,19 @@ class BaseDocument:
except:
pass
return document
def get_all(self, limit: int = None, order_by: str = "dateAdded"):
def get_all(self, limit: int = None, order_by: str = None):
if USE_MONGO:
if order_by:
documents = self.document.objects.order_by(str(order_by)).limit(limit)
elif limit == None:
documents = self.document.objects()
else:
documents = self.document.objects().limit(limit)
docs = []
for item in documents:
doc = BaseDocument._unpack_mongo(item)
docs.append(doc)
docs = [BaseDocument._unpack_mongo(item) for item in documents]
if limit == 1:
return docs[0]
return docs
@ -74,7 +76,7 @@ class BaseDocument:
limit (int, optional): A limit to returned responses. Defaults to 1. \n
Returns:
dict or list[dict]:
dict or list[dict]:
"""
if match_key == None:
match_key = self.primary_key

View file

@ -25,7 +25,7 @@ class _Themes(BaseDocument):
def update(self, data: dict) -> dict:
if USE_MONGO:
colors = ThemeColorsDocument(**data["colors"])
theme_document = SiteThemeDocument.objects.get(name=data.get("name"))
theme_document = self.document.objects.get(name=data.get("name"))
if theme_document:
theme_document.update(set__colors=colors)

View file

@ -5,7 +5,7 @@ from pydantic import BaseModel
class BackupJob(BaseModel):
tag: Optional[str]
template: Optional[str]
template: Optional[List[str]]
class Config:
schema_extra = {

View file

@ -1,11 +1,7 @@
from fastapi import APIRouter, HTTPException
from models.backup_models import BackupJob, Imports
from services.backup_services import (
BACKUP_DIR,
TEMPLATE_DIR,
export_db,
import_from_archive,
)
from services.backups.export import backup_all
from settings import BACKUP_DIR, TEMPLATE_DIR
from utils.snackbar import SnackResponse
router = APIRouter()
@ -28,17 +24,15 @@ async def available_imports():
@router.post("/api/backups/export/database/", tags=["Import / Export"], status_code=201)
async def export_database(data: BackupJob):
"""Generates a backup of the recipe database in json format."""
export_path = backup_all(data.tag, data.template)
try:
export_path = export_db(data.tag, data.template)
return SnackResponse.success("Backup Created at " + export_path)
except:
HTTPException(
status_code=400,
detail=SnackResponse.error("Error Creating Backup. See Log File"),
)
return SnackResponse.success("Backup Created at " + export_path)
@router.post(
"/api/backups/{file_name}/import/", tags=["Import / Export"], status_code=200

View file

@ -1,14 +1,3 @@
from db.tinydb.tinydb_setup import TinyDatabase
from services.backups.export import export_recipes
db = TinyDatabase()
test_object = {"name": "dan", "job": "programmer"}
test_object_2 = {"name": "jennifer", "job": "programmer"}
test_object_3 = {"name": "steve", "job": "programmer"}
db.recipes.delete("jennifer")
db.recipes.delete("dan")
db.recipes.save(test_object)
db.recipes.save(test_object_2)
db.recipes.update_doc("dan", test_object_3)
export_recipes()

View file

@ -1,11 +1,9 @@
import json
import shutil
import zipfile
from datetime import datetime
from pathlib import Path
from db.mongo.recipe_models import RecipeDocument
from jinja2 import Template
from utils.logger import logger
from services.recipe_services import IMG_DIR
@ -16,18 +14,6 @@ TEMPLATE_DIR = CWD.parent.joinpath("data", "templates")
TEMP_DIR = CWD.parent.joinpath("data", "temp")
def auto_backup_job():
for backup in BACKUP_DIR.glob("Auto*.zip"):
backup.unlink()
templates = []
for template in TEMPLATE_DIR.iterdir():
templates.append(template)
export_db(tag="Auto", templates=templates)
logger.info("Auto Backup Called")
def import_migration(recipe_dict: dict) -> dict:
del recipe_dict["_id"]
del recipe_dict["dateAdded"]
@ -72,78 +58,5 @@ def import_from_archive(file_name: str) -> list:
return {"successful": successful_imports, "failed": failed_imports}
def export_db(tag=None, templates=None):
if tag:
export_tag = tag + "_" + datetime.now().strftime("%Y-%b-%d")
else:
export_tag = datetime.now().strftime("%Y-%b-%d")
backup_folder = TEMP_DIR.joinpath(export_tag)
backup_folder.mkdir(parents=True, exist_ok=True)
img_folder = backup_folder.joinpath("images")
img_folder.mkdir(parents=True, exist_ok=True)
recipe_folder = backup_folder.joinpath("recipes")
recipe_folder.mkdir(parents=True, exist_ok=True)
export_images(img_folder)
if type(templates) == list:
for template in templates:
export_recipes(recipe_folder, template)
elif type(templates) == str:
export_recipes(recipe_folder, templates)
else:
export_recipes(recipe_folder)
zip_path = BACKUP_DIR.joinpath(f"{export_tag}")
shutil.make_archive(zip_path, "zip", backup_folder)
shutil.rmtree(backup_folder)
shutil.rmtree(TEMP_DIR)
return str(zip_path.absolute()) + ".zip"
def export_images(dest_dir) -> Path:
for file in IMG_DIR.iterdir():
shutil.copy(file, dest_dir.joinpath(file.name))
def export_recipes(dest_dir: Path, template=None) -> Path:
all_recipes = RecipeDocument.objects()
logger.info(f"Backing Up Recipes: {all_recipes}")
for recipe in all_recipes:
json_recipe = recipe.to_json(indent=4)
if template:
md_dest = dest_dir.parent.joinpath("templates")
md_dest.mkdir(parents=True, exist_ok=True)
template = TEMPLATE_DIR.joinpath(template)
export_markdown(md_dest, json_recipe, template)
filename = recipe.slug + ".json"
file_path = dest_dir.joinpath(filename)
with open(file_path, "w") as f:
f.write(json_recipe)
def export_markdown(dest_dir: Path, recipe_data: json, template=Path) -> Path:
recipe_data: dict = json.loads(recipe_data)
recipe_template = TEMPLATE_DIR.joinpath("recipes.md")
with open(recipe_template, "r") as f:
template = Template(f.read())
out_file = dest_dir.joinpath(recipe_data["slug"] + ".md")
content = template.render(recipe=recipe_data)
with open(out_file, "w") as f:
f.write(content)
if __name__ == "__main__":
pass

View file

@ -0,0 +1,140 @@
import json
import shutil
import zipfile
from datetime import datetime
from pathlib import Path
from jinja2 import Template
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings, SiteTheme
from settings import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from utils.logger import logger
class DatabaseExport:
def __init__(self, tag=None, templates=None) -> None:
if tag:
export_tag = tag + "_" + datetime.now().strftime("%Y-%b-%d")
else:
export_tag = datetime.now().strftime("%Y-%b-%d")
self.main_dir = TEMP_DIR.joinpath(export_tag)
self.img_dir = self.main_dir.joinpath("images")
self.recipe_dir = self.main_dir.joinpath("recipes")
self.themes_dir = self.main_dir.joinpath("themes")
self.settings_dir = self.main_dir.joinpath("settings")
self.templates_dir = self.main_dir.joinpath("templates")
self.mealplans_dir = self.main_dir.joinpath("mealplans")
try:
self.templates = [TEMPLATE_DIR.joinpath(x) for x in templates]
except:
self.templates = False
logger.info("No Jinja2 Templates Registered for Export")
required_dirs = [
self.main_dir,
self.img_dir,
self.recipe_dir,
self.themes_dir,
self.settings_dir,
self.templates_dir,
self.mealplans_dir,
]
for dir in required_dirs:
dir.mkdir(parents=True, exist_ok=True)
def export_recipes(self):
all_recipes = Recipe.get_all()
for recipe in all_recipes:
logger.info(f"Backing Up Recipes: {recipe}")
filename = recipe.get("slug") + ".json"
file_path = self.recipe_dir.joinpath(filename)
DatabaseExport._write_json_file(recipe, file_path)
if self.templates:
self._export_template(recipe)
def _export_template(self, recipe_data: dict):
for template_path in self.templates:
with open(template_path, "r") as f:
template = Template(f.read())
filename = recipe_data.get("name") + template_path.suffix
out_file = self.templates_dir.joinpath(filename)
content = template.render(recipe=recipe_data)
with open(out_file, "w") as f:
f.write(content)
def export_images(self):
for file in IMG_DIR.iterdir():
shutil.copy(file, self.img_dir.joinpath(file.name))
def export_settings(self):
all_settings = SiteSettings.get_site_settings()
out_file = self.settings_dir.joinpath("settings.json")
DatabaseExport._write_json_file(all_settings.dict(), out_file)
def export_themes(self):
all_themes = SiteTheme.get_all()
print(all_themes)
if all_themes:
all_themes = [x.dict() for x in all_themes]
out_file = self.themes_dir.joinpath("themes.json")
DatabaseExport._write_json_file(all_themes, out_file)
# def export_meals(self): #! Problem Parseing Datetime Objects... May come back to this
# meal_plans = MealPlan.get_all()
# if meal_plans:
# meal_plans = [x.dict() for x in meal_plans]
# print(meal_plans)
# out_file = self.mealplans_dir.joinpath("mealplans.json")
# DatabaseExport._write_json_file(meal_plans, out_file)
@staticmethod
def _write_json_file(data, out_file: Path):
json_data = json.dumps(data, indent=4)
with open(out_file, "w") as f:
f.write(json_data)
def finish_export(self):
zip_path = BACKUP_DIR.joinpath(f"{self.main_dir.name}")
shutil.make_archive(zip_path, "zip", self.main_dir)
shutil.rmtree(TEMP_DIR)
return str(zip_path.absolute()) + ".zip"
def backup_all(tag=None, templates=None):
db_export = DatabaseExport(tag=tag, templates=templates)
db_export.export_recipes()
db_export.export_images()
db_export.export_settings()
db_export.export_themes()
# db_export.export_meals()
return db_export.finish_export()
def auto_backup_job():
for backup in BACKUP_DIR.glob("Auto*.zip"):
backup.unlink()
templates = []
for template in TEMPLATE_DIR.iterdir():
templates.append(template)
backup_all(tag="Auto", templates=templates)
logger.info("Auto Backup Called")

View file

@ -0,0 +1,9 @@
import json
import shutil
import zipfile
from pathlib import Path
from utils.logger import logger
from settings import IMG_DIR, BACKUP_DIR, TEMPLATE_DIR, TEMP_DIR

View file

@ -1,4 +1,3 @@
import json
from datetime import date, timedelta
from pathlib import Path
from typing import List, Optional

View file

@ -134,6 +134,10 @@ class Recipe(BaseModel):
def update_image(slug: str, extension: str):
db.recipes.update_image(slug, extension)
@staticmethod
def get_all():
return db.recipes.get_all()
def read_requested_values(keys: list, max_results: int = 0) -> List[dict]:
"""

View file

@ -5,7 +5,7 @@ import requests
from apscheduler.schedulers.background import BackgroundScheduler
from utils.logger import logger
from services.backup_services import auto_backup_job
from services.backups.export import auto_backup_job
from services.meal_services import MealPlan
from services.recipe_services import Recipe
from services.settings_services import SiteSettings

View file

@ -29,6 +29,10 @@ class SiteSettings(BaseModel):
}
}
@staticmethod
def get_all():
db.settings.get_all()
@classmethod
def get_site_settings(cls):
try:
@ -137,4 +141,4 @@ def default_theme_init():
default_theme.save_to_db()
# default_theme_init()
default_theme_init()