From 68d06ff3f88154d58f07399f2b04c75a78280bc4 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Tue, 7 May 2024 15:57:22 +0500 Subject: [PATCH] core: fix legacy backup restoration --- packages/core/src/database/backup.ts | 62 +++++++++++++++++++++----- packages/core/src/database/migrator.ts | 22 ++++----- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/packages/core/src/database/backup.ts b/packages/core/src/database/backup.ts index ac19be4d4..81cd32589 100644 --- a/packages/core/src/database/backup.ts +++ b/packages/core/src/database/backup.ts @@ -22,10 +22,12 @@ import { CURRENT_DATABASE_VERSION } from "../common.js"; import Migrator from "./migrator.js"; import Database from "../api/index.js"; import { + Attachment, Item, MaybeDeletedItem, Note, Notebook, + Relation, ValueOf, isDeleted } from "../types.js"; @@ -36,6 +38,7 @@ import { DatabaseCollection } from "./index.js"; import { DefaultColors } from "../collections/colors.js"; import { toChunks } from "../utils/array.js"; import { logger } from "../logger.js"; +import { clone } from "../utils/clone.js"; type BackupDataItem = MaybeDeletedItem | string[]; type BackupPlatform = "web" | "mobile" | "node"; @@ -92,10 +95,44 @@ function isEncryptedBackup( return "encrypted" in backup ? backup.encrypted : isCipher(backup.data); } -function isLegacyBackupFile( - backup: LegacyBackupFile | BackupFile -): backup is LegacyBackupFile { - return backup.version <= 5.8; +/** + * Due to a bug in v3.0, legacy backups were created with version set to 6.1 + * while their actual data was at version 5.9. This caused various issues when + * restoring such a backup. + * This function tries to work around that bug by detecting the version based on + * the actual data. + */ +function isLegacyBackup(data: BackupDataItem[]) { + const note = data.find( + (c): c is Note => !isDeleted(c) && !Array.isArray(c) && c.type === "note" + ); + if (note) + return ( + "color" in note || + "notebooks" in note || + "tags" in note || + "locked" in note + ); + + const notebook = data.find( + (c): c is Notebook => + !isDeleted(c) && !Array.isArray(c) && c.type === "notebook" + ); + if (notebook) return "topics" in notebook; + + const attachment = data.find( + (c): c is Attachment => + !isDeleted(c) && !Array.isArray(c) && c.type === "attachment" + ); + if (attachment) return "noteIds" in attachment; + + const relation = data.find( + (c): c is Relation => + !isDeleted(c) && !Array.isArray(c) && c.type === "relation" + ); + if (relation) return "from" in relation || "to" in relation; + + return false; } const MAX_CHUNK_SIZE = 10 * 1024 * 1024; @@ -221,7 +258,7 @@ export default class Backup { yield { path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`, data: `{ -"version": ${CURRENT_DATABASE_VERSION}, +"version": 5.9, "type": "${type}", "date": ${Date.now()}, "data": ${itemsJSON}, @@ -397,13 +434,16 @@ export default class Backup { if (!data) throw new Error("No data found."); + const normalizedData: BackupDataItem[] = Array.isArray(data) + ? (data as BackupDataItem[]) + : typeof data === "object" + ? Object.values(data) + : []; await this.migrateData( - Array.isArray(data) - ? (data as BackupDataItem[]) - : typeof data === "object" - ? Object.values(data) - : [], - backup.version + normalizedData, + backup.version === 6.1 && isLegacyBackup(normalizedData) + ? 5.9 + : backup.version ); } diff --git a/packages/core/src/database/migrator.ts b/packages/core/src/database/migrator.ts index 2feb58baf..fc8de69fd 100644 --- a/packages/core/src/database/migrator.ts +++ b/packages/core/src/database/migrator.ts @@ -41,6 +41,7 @@ import { } from "../types"; import { IndexedCollection } from "./indexed-collection"; import { SQLCollection } from "./sql-collection"; +import { logger } from "../logger"; export type RawItem = MaybeDeletedItem; type MigratableCollection = { @@ -136,10 +137,16 @@ class Migrator { for (let i = 0; i < items.length; ++i) { const item = items[i]; // can be true due to corrupted data. - if (Array.isArray(item)) continue; + if (Array.isArray(item)) { + logger.debug("Skipping item during migration to SQLite", { + table, + version, + item + }); + continue; + } if (!item) continue; - const itemId = item.id; let migrated = await migrateItem( item, version, @@ -161,14 +168,7 @@ class Migrator { ); } - if (migrated === true) { - toAdd.push(item); - - // if id changed after migration, we need to delete the old one. - if (item.id !== itemId) { - // await collection.deleteItem(itemId); - } - } + if (migrated !== "skip") toAdd.push(item); } if (toAdd.length > 0) { @@ -217,7 +217,7 @@ class Migrator { ); } - if (!migrated) continue; + if (!migrated || migrated === "skip") continue; toAdd.push(item);