Skip to content

Commit

Permalink
Fixed backups
Browse files Browse the repository at this point in the history
  • Loading branch information
jlucaspains committed Dec 16, 2024
1 parent 781b5ec commit 8ae4212
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 80 deletions.
10 changes: 9 additions & 1 deletion src/pages/recipe/backupModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Category } from "../../services/category";
import { Recipe } from "../../services/recipe";

export class BackupModel extends Recipe {
export class RecipeBackupModel extends Recipe {
constructor() {
super();

Expand All @@ -10,4 +11,11 @@ export class BackupModel extends Recipe {
}

media?: Array<{type: string, url: string}>;
category?: string;
}

export class BackupModel {
recipes: RecipeBackupModel[] = [];
categories: Category[] = [];
version: number = 2;
}
49 changes: 22 additions & 27 deletions src/pages/recipe/import-backup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
import { onMounted, ref } from "vue";
import { useState } from "../../services/store";
import { fileOpen } from "browser-fs-access";
import { saveRecipe, saveRecipeMedia } from "../../services/dataService";
import { saveCategory, saveRecipe, saveRecipeMedia } from "../../services/dataService";
import { notify } from "notiwind";
import { Recipe, RecipeMedia, RecipeNutrition } from "../../services/recipe";
import { useTranslation } from "i18next-vue";
import BusyIndicator from "../../components/BusyIndicator.vue";
import { fetchWithRetry } from "../../services/fetchWithRetry";
import i18next from "i18next";
const state = useState()!;
const importItemsDisplay = ref([] as Array<{ isSelected: boolean, title: string }>);
const canSave = ref(false);
let importItems = [] as Array<any>;
let importRecipes = [] as Array<any>;
let importCategories = [] as Array<any>;
const { t } = useTranslation();
const isBusy = ref(false);
Expand All @@ -23,8 +23,11 @@ onMounted(() => {
state.menuOptions = [];
});
function saveRecipes() {
importItems.forEach(async (recipe, index) => {
function saveBackup() {
importCategories.forEach(async (category) => {
await saveCategory(category);
});
importRecipes.forEach(async (recipe, index) => {
if (!importItemsDisplay.value[index].isSelected) {
return;
}
Expand All @@ -40,6 +43,7 @@ function saveRecipes() {
?? new RecipeNutrition(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
parsedRecipe.language = recipe.language
?? i18next.language;
parsedRecipe.categoryId = recipe.categoryId || 0;
const id = await saveRecipe(parsedRecipe);
Expand All @@ -63,49 +67,40 @@ function saveRecipes() {
async function pickFile() {
importItemsDisplay.value = [];
importItems = [];
importRecipes = [];
canSave.value = false;
// Open a file.
const filePicked = await fileOpen({
mimeTypes: ["application/zip", "application/json"],
mimeTypes: ["application/json"],
});
let success = true;
try {
let result;
if (filePicked.name.endsWith(".zip")) {
isBusy.value = true;
const data = new FormData();
data.append('file', filePicked);
const response = await fetchWithRetry("/api/process-backup", {
method: "POST",
body: data
});
success = response.ok;
if (!success) {
return;
}
const textResult = await filePicked.text();
result = JSON.parse(textResult);
result = await response.json();
if (Array.isArray(result)) {
// version 1 of the backup file
importRecipes = result;
importCategories = [];
} else {
const textResult = await filePicked.text();
result = JSON.parse(textResult);
// version 2 (new) of the backup file
importRecipes = result.recipes;
importCategories = result.categories;
}
importItems = result;
importItemsDisplay.value = importItems.map(item => {
importItemsDisplay.value = importRecipes.map(item => {
return { isSelected: true, title: item.title };
});
canSave.value = true;
state.menuOptions = [{
text: "Save",
action: saveRecipes,
action: saveBackup,
svg: `<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" /> <polyline points="17 21 17 13 7 13 7 21" /> <polyline points="7 3 7 8 15 8" />`,
}];
}
Expand Down
34 changes: 21 additions & 13 deletions src/services/dataService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dexie, Table } from "dexie";
import { BackupModel } from "../pages/recipe/backupModel"
import { BackupModel, RecipeBackupModel } from "../pages/recipe/backupModel"
import { Recipe, RecipeImage, RecipeMedia, RecipeNutrition } from "./recipe";
import { Setting } from "./setting";
import { Category } from "./category";
Expand Down Expand Up @@ -174,39 +174,47 @@ export async function getSetting(name: string, defaultValue: string): Promise<st
return result;
}

export async function prepareBackup(): Promise<Array<BackupModel>> {
export async function prepareBackup(): Promise<BackupModel> {
const allRecipes = await db.recipes.toArray();
const allMedia = await db.recipeMedia.toArray();
const allCategories = await db.categories.toArray();

const result = [];
const result = new BackupModel();
for (const recipe of allRecipes) {
const model = getBackupModel(recipe, allMedia);
const category = allCategories.find(item => item.id == recipe.categoryId);
const model = getBackupModel(recipe, category, allMedia);

result.push(model);
result.recipes.push(model);
}

result.categories = allCategories;

return result;
}


export async function prepareRecipeBackup(id: number): Promise<Array<BackupModel>> {
export async function prepareRecipeBackup(id: number): Promise<BackupModel> {
const recipe = await db.recipes.get(id);
const allMedia = await db.recipeMedia.where("recipeId").equals(id).toArray();
const category = await db.categories.get(recipe?.categoryId || 0);

const result: Array<BackupModel> = [];
const result = new BackupModel();

if (!recipe) {
return result;
}

const model = getBackupModel(recipe, allMedia);
result.push(model);
const model = getBackupModel(recipe, category, allMedia);
result.recipes.push(model);

if (category) {
result.categories.push(category);
}

return result;
}

function getBackupModel(recipe: Recipe, allMedia: RecipeMedia[]): BackupModel {
const model = new BackupModel();
function getBackupModel(recipe: Recipe, category: Category | undefined, allMedia: RecipeMedia[]): RecipeBackupModel {
const model = new RecipeBackupModel();
model.id = recipe.id;
model.title = recipe.title;
model.ingredients = recipe.ingredients;
Expand All @@ -225,7 +233,7 @@ function getBackupModel(recipe: Recipe, allMedia: RecipeMedia[]): BackupModel {
};
});
model.categoryId = recipe.categoryId;
// TODO: how to export folders?
model.category = category?.name;

return model;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/display.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ Bake it for 30 min`;
test('share as file', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'not applicable');
await page.addInitScript(() => {
const comparer = '[{"id":2,"title":"New Bread","score":5,"ingredients":["100g flour"],"steps":["Bake it for 30 min"],"notes":"","multiplier":1,"nutrition":{"servingSize":0,"totalFat":0,"saturatedFat":0,"sodium":0,"protein":0,"cholesterol":0,"calories":0,"carbohydrates":0,"fiber":0,"sugar":0,"transFat":0,"unsaturatedFat":0},"categoryId":0,"media":[]}]';
const comparer = '{"recipes":[{"id":2,"title":"New Bread","score":5,"ingredients":["100g flour"],"steps":["Bake it for 30 min"],"notes":"","multiplier":1,"nutrition":{"servingSize":0,"totalFat":0,"saturatedFat":0,"sodium":0,"protein":0,"cholesterol":0,"calories":0,"carbohydrates":0,"fiber":0,"sugar":0,"transFat":0,"unsaturatedFat":0},"categoryId":0,"media":[]}],"categories":[],"version":2}';
const stream = new WritableStream({
write(chunk) {
return new Promise(async (resolve, reject) => {
const blob = new Blob([chunk]);
const result = await blob.text()
const json = JSON.parse(result);
delete json[0].changedOn;
delete json.recipes[0].changedOn;

if (JSON.stringify(json) !== comparer) {
console.error(JSON.stringify(json));
Expand Down
92 changes: 58 additions & 34 deletions tests/import-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => {
});
});

test('Restore json backup old format', async ({ page }) => {
test('Restore v1 json backup old format', async ({ page }) => {
await page.addInitScript(() => {

const blob = new Blob([`[
Expand Down Expand Up @@ -47,7 +47,7 @@ test('Restore json backup old format', async ({ page }) => {
expect(await page.getByText('New Bread Recipe').textContent()).toEqual("New Bread Recipe");
});

test('Restore json backup new format', async ({ page }) => {
test('Restore v1 json backup new format', async ({ page }) => {
await page.addInitScript(() => {

const blob = new Blob([`[
Expand Down Expand Up @@ -86,7 +86,7 @@ test('Restore json backup new format', async ({ page }) => {
expect(await page.getByText('New Bread Recipe').textContent()).toEqual("New Bread Recipe");
});

test('Restore json backup with video', async ({ page }) => {
test('Restore v1 json backup with video', async ({ page }) => {
await page.addInitScript(() => {

const blob = new Blob([`[
Expand Down Expand Up @@ -128,53 +128,77 @@ test('Restore json backup with video', async ({ page }) => {
.toHaveAttribute("src", "https://www.youtube.com/embed/0YY7K7Xa5rE");
});

test('Restore zip backup', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'not applicable');

test('Restore v2 json backup new format', async ({ page }) => {
await page.addInitScript(() => {

const blob = new Blob([], { type: 'application/zip' });
const file = new File([blob], "file.zip", { type: "application/zip" });

const fileHandle = {
getFile: async () => { return file; }
};

(window as any).showOpenFilePicker = async (param: any) => {
return [fileHandle];
};
});

const response = `[
const blob = new Blob([`{
"recipes": [
{
"id": 1,
"id": 2,
"title": "New Bread Recipe",
"score": 5,
"changedOn": "2022-12-29T00:35:42.073Z",
"source": "Breadtopia",
"ingredients": [
"142g whole wheat flour"
"142g whole wheat flour",
"312g white bread flour",
"7.1g salt",
"354g purified water",
"80g starter"
],
"steps": [
"Mix together the dry ingredients"
"bake"
],
"notes": "May replace whole wheat flour with rye for added taste",
"notes": "",
"multiplier": 1,
"image": "/bread.jpg",
"imageAvailable": true,
"images": [],
"mutrition": {"servingSize": 1, "calories": 0}
"nutrition": {
"servingSize": 0,
"totalFat": 0,
"saturatedFat": 0,
"sodium": 0,
"protein": 0,
"cholesterol": 0,
"calories": 0,
"carbohydrates": 0,
"fiber": 0,
"sugar": 0,
"transFat": 0,
"unsaturatedFat": 0
},
"categoryId": 1,
"media": [],
"category": "Bread"
}
]`;
],
"categories": [
{
"name": "Bread",
"image": "https://via.placeholder.com/150",
"id": 1
}
],
"version": 2
}`], { type: 'application/json' });
const file = new File([blob], "file.json", { type: "application/json" });

await page.route(/.*\/api\/process-backup/, async route => {
const json = JSON.parse(response);
await route.fulfill({ json });
const fileHandle = {
getFile: async () => { return file; }
};

(window as any).showOpenFilePicker = async (param: any) => {
return [fileHandle];
};
});

await page.goto('#/preview-features');
await page.getByTestId('enable-category-toggle').click();

await page.goto('#/recipe/import-backup');
await page.getByTestId("import-button").click();
await page.getByTestId("topbar-single-button").click();
await page.goto('#/recipe/1');

await page.goto('/');
expect(await page.getByText('Bread').textContent()).toEqual("Bread");

await page.getByText('Bread').click();
expect(await page.getByText('New Bread Recipe').textContent()).toEqual("New Bread Recipe");
});
});
Loading

0 comments on commit 8ae4212

Please sign in to comment.