From d6830c548862633d9aedfe0415c3e0892b2417de Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 12 Feb 2024 11:38:35 +0500 Subject: [PATCH] core: add support for creating backups of legacy collections --- packages/core/src/api/user-manager.ts | 18 ++++++ packages/core/src/database/backup.ts | 88 +++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index b907148bf..79ec93ccd 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -279,6 +279,13 @@ class UserManager { return this.db.kv().read("user"); } + /** + * @deprecated + */ + getLegacyUser() { + return this.db.storage().read("user"); + } + async resetUser(removeAttachments = true) { const token = await this.tokenManager.getAccessToken(); if (!token) return; @@ -371,6 +378,17 @@ class UserManager { return { key, salt: user.salt }; } + /** + * @deprecated + */ + async getLegacyEncryptionKey(): Promise { + const user = await this.getLegacyUser(); + if (!user) return; + const key = await this.db.storage().getCryptoKey(); + if (!key) return; + return { key, salt: user.salt }; + } + async getAttachmentsKey() { try { let user = await this.getUser(); diff --git a/packages/core/src/database/backup.ts b/packages/core/src/database/backup.ts index 7bd6d2029..a7922db44 100644 --- a/packages/core/src/database/backup.ts +++ b/packages/core/src/database/backup.ts @@ -34,6 +34,7 @@ import { isCipher } from "./crypto.js"; import { migrateItem } from "../migrations"; import { DatabaseCollection } from "./index.js"; import { DefaultColors } from "../collections/colors.js"; +import { toChunks } from "../utils/array.js"; type BackupDataItem = MaybeDeletedItem | string[]; type BackupPlatform = "web" | "mobile" | "node"; @@ -157,7 +158,94 @@ export default class Backup { await this.db.kv().write("lastBackupTime", Date.now()); } + /** + * @deprecated + */ + async *exportLegacy(type: BackupPlatform, encrypt = false) { + if (!validTypes.some((t) => t === type)) + throw new Error("Invalid type. It must be one of 'mobile' or 'web'."); + if (encrypt && !(await this.db.user.getLegacyUser())) + throw new Error("Please login to create encrypted backups."); + + const key = await this.db.user.getLegacyEncryptionKey(); + if (encrypt && !key) throw new Error("No encryption key found."); + + const keys = await this.db.storage().getAllKeys(); + const chunks = toChunks(keys, 20); + let buffer: string[] = []; + let bufferLength = 0; + const MAX_CHUNK_SIZE = 10 * 1024 * 1024; + let chunkIndex = 0; + + while (chunks.length > 0) { + const chunk = chunks.pop(); + if (!chunk) break; + + const items = await this.db.storage().readMulti(chunk); + items.forEach(([id, item]) => { + const isDeleted = + item && + typeof item === "object" && + "deleted" in item && + !("type" in item); + + if ( + !item || + invalidKeys.includes(id) || + isDeleted || + id.startsWith("_uk_") + ) + return; + + const data = JSON.stringify(item); + buffer.push(data); + bufferLength += data.length; + }); + + if (bufferLength >= MAX_CHUNK_SIZE || chunks.length === 0) { + let itemsJSON = `[${buffer.join(",")}]`; + + buffer = []; + bufferLength = 0; + + itemsJSON = await this.db.compressor().compress(itemsJSON); + + const hash = SparkMD5.hash(itemsJSON); + + if (encrypt && key) + itemsJSON = JSON.stringify( + await this.db.storage().encrypt(key, itemsJSON) + ); + else itemsJSON = JSON.stringify(itemsJSON); + + yield { + path: `${chunkIndex++}-${encrypt ? "encrypted" : "plain"}-${hash}`, + data: `{ +"version": ${CURRENT_DATABASE_VERSION}, +"type": "${type}", +"date": ${Date.now()}, +"data": ${itemsJSON}, +"hash": "${hash}", +"hash_type": "md5", +"compressed": true, +"encrypted": ${encrypt ? "true" : "false"} +}` + }; + } + } + + if (bufferLength > 0 || buffer.length > 0) + throw new Error("Buffer not empty."); + + await this.updateBackupTime(); + } + async *export(type: BackupPlatform, encrypt = false) { + if (this.db.migrations.version === 5.9) { + yield* this.exportLegacy(type, encrypt); + return; + } + if (!validTypes.some((t) => t === type)) throw new Error("Invalid type. It must be one of 'mobile' or 'web'."); if (encrypt && !(await this.db.user.getUser()))