Files
notesnook/packages/core/database/backup.js

249 lines
6.2 KiB
JavaScript
Raw Normal View History

/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2022 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2022-08-30 16:13:11 +05:00
import Migrator from "./migrator.js";
import {
CHECK_IDS,
checkIsUserPremium,
CURRENT_DATABASE_VERSION
} from "../common.js";
import SparkMD5 from "spark-md5";
const invalidKeys = ["user", "t", "v", "lastBackupTime", "lastSynced"];
const invalidIndices = ["tags", "colors"];
const validTypes = ["mobile", "web", "node"];
2020-09-13 13:24:24 +05:00
export default class Backup {
/**
*
* @param {import("../api/index.js").default} db
*/
constructor(db) {
this._db = db;
this._migrator = new Migrator();
2020-09-13 13:24:24 +05:00
}
2020-10-03 12:26:39 +05:00
lastBackupTime() {
return this._db.storage.read("lastBackupTime");
2020-10-03 12:26:39 +05:00
}
/**
*
* @param {"web"|"mobile"|"node"} type
* @param {boolean} encrypt
*/
async export(type, encrypt = false) {
if (encrypt && !(await checkIsUserPremium(CHECK_IDS.backupEncrypt))) {
throw new Error(
"Please upgrade your plan to Pro to use encrypted backups."
);
}
if (!validTypes.some((t) => t === type))
2020-11-24 12:53:52 +05:00
throw new Error("Invalid type. It must be one of 'mobile' or 'web'.");
let keys = await this._db.storage.getAllKeys();
let data = filterData(
Object.fromEntries(await this._db.storage.readMulti(keys))
);
let hash = {};
2020-09-13 13:24:24 +05:00
if (encrypt) {
2020-12-16 12:06:25 +05:00
const key = await this._db.user.getEncryptionKey();
data = await this._db.storage.encrypt(key, JSON.stringify(data));
} else {
hash = { hash: SparkMD5.hash(JSON.stringify(data)), hash_type: "md5" };
2020-09-13 13:24:24 +05:00
}
2020-10-03 12:26:39 +05:00
// save backup time
await this._db.storage.write("lastBackupTime", Date.now());
return JSON.stringify({
version: CURRENT_DATABASE_VERSION,
type,
date: Date.now(),
2020-11-24 12:53:52 +05:00
data,
...hash
});
2020-09-13 13:24:24 +05:00
}
/**
*
* @param {any} backup the backup data
*/
async import(backup, password) {
if (!backup) return;
if (!this._validate(backup)) throw new Error("Invalid backup.");
2020-11-24 17:30:07 +05:00
backup = this._migrateBackup(backup);
let db = backup.data;
const isEncrypted = db.salt && db.iv && db.cipher;
if (isEncrypted) {
if (!password)
throw new Error(
"Please provide a password to decrypt this backup & restore it."
);
const key = await this._db.storage.generateCryptoKey(password, db.salt);
if (!key)
throw new Error("Could not generate encryption key for backup.");
try {
const decrypted = await this._db.storage.decrypt(key, db);
backup.data = JSON.parse(decrypted);
} catch (e) {
if (
e.message.includes("ciphertext cannot be decrypted") ||
e.message === "FAILURE"
)
throw new Error("Incorrect password.");
throw new Error(`Could not decrypt backup: ${e.message}`);
}
} else if (!this._verify(backup))
throw new Error("Backup file has been tempered, aborting...");
2020-11-24 17:30:07 +05:00
await this._migrateData(backup);
}
_migrateBackup(backup) {
const { version = 0 } = backup;
if (version > CURRENT_DATABASE_VERSION)
2020-11-24 17:30:07 +05:00
throw new Error(
"This backup was made from a newer version of Notesnook. Cannot migrate."
);
switch (version) {
case CURRENT_DATABASE_VERSION:
2022-07-15 19:31:18 +05:00
case 5.5:
2022-07-07 13:17:55 +05:00
case 5.4:
case 5.3:
case 5.2:
case 5.1:
case 5.0: {
2020-11-24 17:30:07 +05:00
return backup;
}
default:
throw new Error("Unknown backup version.");
}
}
async _migrateData(backup) {
const { data, version = 0 } = backup;
if (version > CURRENT_DATABASE_VERSION)
2020-11-24 17:30:07 +05:00
throw new Error(
"This backup was made from a newer version of Notesnook. Cannot migrate."
);
// we have to reindex to make sure we handle all the items
// properly.
reindex(data);
2020-11-24 17:30:07 +05:00
const collections = [
{
index: data["attachments"],
dbCollection: this._db.attachments
},
{
index: data["notebooks"],
dbCollection: this._db.notebooks
},
{
index: data["content"],
dbCollection: this._db.content
},
2022-09-07 12:47:02 +05:00
{
index: data["shortcuts"],
2022-09-07 14:13:26 +05:00
dbCollection: this._db.shortcuts
2022-09-07 12:47:02 +05:00
},
{
index: data["notes"],
dbCollection: this._db.notes
},
{
index: ["settings"],
dbCollection: this._db.settings,
type: "settings"
}
2020-11-24 17:30:07 +05:00
];
2021-11-18 19:40:59 +05:00
await this._db.syncer.acquireLock(async () => {
await this._migrator.migrate(collections, (id) => data[id], version);
2021-11-18 19:40:59 +05:00
});
2020-09-13 13:24:24 +05:00
}
_validate(backup) {
return (
!!backup.date &&
!!backup.data &&
!!backup.type &&
validTypes.some((t) => t === backup.type)
);
}
2020-11-24 12:53:52 +05:00
_verify(backup) {
const { hash, hash_type, data: db } = backup;
switch (hash_type) {
case "md5": {
return hash === SparkMD5.hash(JSON.stringify(db));
}
default: {
return false;
}
}
}
2020-09-13 13:24:24 +05:00
}
function filterData(data) {
let skippedKeys = [...invalidKeys, ...invalidIndices];
invalidIndices.forEach((key) => {
const index = data[key];
if (!index) return;
skippedKeys.push(...index);
});
skippedKeys.forEach((key) => delete data[key]);
return data;
}
function reindex(data) {
for (let key in data) {
const item = data[key];
if (!item) {
delete data[key];
continue;
}
switch (item.type) {
case "notebook":
if (!data["notebooks"]) data["notebooks"] = [];
data["notebooks"].push(item.id);
break;
case "note":
if (!data["notes"]) data["notes"] = [];
data["notes"].push(item.id);
break;
case "content":
if (!data["content"]) data["content"] = [];
data["content"].push(item.id);
break;
}
}
}