2020-11-16 15:00:24 +05:00
|
|
|
import Hashes from "jshashes";
|
2020-12-06 10:52:00 +05:00
|
|
|
import Migrator from "./migrator.js";
|
2020-12-05 15:28:54 +05:00
|
|
|
import {
|
|
|
|
|
CHECK_IDS,
|
|
|
|
|
sendCheckUserStatusEvent,
|
|
|
|
|
CURRENT_DATABASE_VERSION,
|
|
|
|
|
} from "../common.js";
|
2020-11-16 15:00:24 +05:00
|
|
|
const md5 = new Hashes.MD5();
|
2020-10-03 11:59:20 +05:00
|
|
|
|
2020-10-28 10:58:07 +05:00
|
|
|
const invalidKeys = ["user", "t", "lastBackupTime"];
|
2020-10-03 11:59:20 +05:00
|
|
|
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;
|
2020-12-06 10:52:00 +05:00
|
|
|
this._migrator = new Migrator();
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|
|
|
|
|
|
2020-10-03 12:26:39 +05:00
|
|
|
lastBackupTime() {
|
|
|
|
|
return this._db.context.read("lastBackupTime");
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-03 11:59:20 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {"web"|"mobile"|"node"} type
|
|
|
|
|
* @param {boolean} encrypt
|
|
|
|
|
*/
|
|
|
|
|
async export(type, encrypt = false) {
|
2020-12-05 13:59:27 +05:00
|
|
|
if (encrypt && !(await sendCheckUserStatusEvent(CHECK_IDS.backupEncrypt)))
|
|
|
|
|
return;
|
2020-11-11 15:43:09 +05:00
|
|
|
|
2020-10-03 11:59:20 +05:00
|
|
|
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'.");
|
2020-10-03 11:59:20 +05:00
|
|
|
|
|
|
|
|
const keys = (await this._db.context.getAllKeys()).filter(
|
2020-10-28 13:22:28 +05:00
|
|
|
(key) => !invalidKeys.some((t) => t === key)
|
2020-10-03 11:59:20 +05:00
|
|
|
);
|
|
|
|
|
|
2020-11-24 12:53:52 +05:00
|
|
|
let data = Object.fromEntries(await this._db.context.readMulti(keys));
|
2020-10-03 11:59:20 +05:00
|
|
|
|
2020-09-13 13:24:24 +05:00
|
|
|
if (encrypt) {
|
|
|
|
|
const key = await this._db.user.key();
|
2020-11-24 12:53:52 +05:00
|
|
|
data = await this._db.context.encrypt(key, JSON.stringify(data));
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|
2020-10-03 11:59:20 +05:00
|
|
|
|
2020-10-03 12:26:39 +05:00
|
|
|
// save backup time
|
|
|
|
|
await this._db.context.write("lastBackupTime", Date.now());
|
|
|
|
|
|
2020-10-03 11:59:20 +05:00
|
|
|
return JSON.stringify({
|
2020-12-05 15:28:54 +05:00
|
|
|
version: CURRENT_DATABASE_VERSION,
|
2020-10-03 11:59:20 +05:00
|
|
|
type,
|
|
|
|
|
date: Date.now(),
|
2020-11-24 12:53:52 +05:00
|
|
|
data,
|
|
|
|
|
hash: md5.hex(JSON.stringify(data)),
|
|
|
|
|
hash_type: "md5",
|
2020-10-03 11:59:20 +05:00
|
|
|
});
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|
|
|
|
|
|
2020-10-03 11:59:20 +05:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {string} data the backup data
|
|
|
|
|
*/
|
2020-09-13 13:24:24 +05:00
|
|
|
async import(data) {
|
2020-10-03 11:59:20 +05:00
|
|
|
if (!data) return;
|
|
|
|
|
|
2020-09-13 13:24:24 +05:00
|
|
|
let backup = JSON.parse(data);
|
2020-10-03 11:59:20 +05:00
|
|
|
|
|
|
|
|
if (!this._validate(backup)) throw new Error("Invalid backup.");
|
|
|
|
|
|
2020-11-24 17:30:07 +05:00
|
|
|
backup = this._migrateBackup(backup);
|
|
|
|
|
|
2020-10-03 11:59:20 +05:00
|
|
|
let db = backup.data;
|
2020-09-13 13:24:24 +05:00
|
|
|
//check if we have encrypted data
|
2020-10-03 11:59:20 +05:00
|
|
|
if (db.salt && db.iv) {
|
2020-09-13 13:24:24 +05:00
|
|
|
const key = await this._db.user.key();
|
2020-10-03 11:59:20 +05:00
|
|
|
db = JSON.parse(await this._db.context.decrypt(key, db));
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|
2020-10-03 11:59:20 +05:00
|
|
|
|
2020-11-24 12:53:52 +05:00
|
|
|
if (!this._verify(backup))
|
2020-10-03 11:59:20 +05:00
|
|
|
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;
|
2020-12-05 15:28:54 +05:00
|
|
|
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) {
|
2020-12-05 15:28:54 +05:00
|
|
|
case CURRENT_DATABASE_VERSION:
|
2020-12-07 12:02:01 +05:00
|
|
|
case 3:
|
2020-12-05 13:59:27 +05:00
|
|
|
case 2: {
|
2020-11-24 17:30:07 +05:00
|
|
|
return backup;
|
|
|
|
|
}
|
|
|
|
|
case 0: {
|
|
|
|
|
const hash = backup.data.h;
|
|
|
|
|
const hash_type = backup.data.ht;
|
|
|
|
|
delete backup.data.h;
|
|
|
|
|
delete backup.data.ht;
|
|
|
|
|
return {
|
|
|
|
|
version: 0,
|
|
|
|
|
type: backup.type,
|
|
|
|
|
date: backup.date || Date.now(),
|
|
|
|
|
data: backup.data,
|
|
|
|
|
hash,
|
|
|
|
|
hash_type,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("Unknown backup version.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _migrateData(backup) {
|
|
|
|
|
const { data, version = 0 } = backup;
|
2020-12-06 10:52:00 +05:00
|
|
|
|
2020-12-05 15:28:54 +05:00
|
|
|
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."
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const collections = [
|
2020-12-06 10:52:00 +05:00
|
|
|
{
|
|
|
|
|
index: data["notes"],
|
|
|
|
|
dbCollection: this._db.notes,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["notebooks"],
|
|
|
|
|
dbCollection: this._db.notebooks,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["tags"],
|
|
|
|
|
dbCollection: this._db.tags,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["colors"],
|
|
|
|
|
dbCollection: this._db.colors,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["trash"],
|
|
|
|
|
dbCollection: this._db.trash,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["delta"],
|
|
|
|
|
dbCollection: this._db.content,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: data["content"],
|
|
|
|
|
dbCollection: this._db.content,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
index: ["settings"],
|
|
|
|
|
dbCollection: this._db.settings,
|
|
|
|
|
},
|
2020-11-24 17:30:07 +05:00
|
|
|
];
|
|
|
|
|
|
2020-12-06 10:52:00 +05:00
|
|
|
await this._migrator.migrate(collections, (id) => data[id], version);
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|
2020-10-03 11:59:20 +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;
|
2020-10-28 13:25:10 +05:00
|
|
|
switch (hash_type) {
|
2020-11-16 15:00:24 +05:00
|
|
|
case "md5": {
|
2020-11-24 12:53:52 +05:00
|
|
|
return hash === md5.hex(JSON.stringify(db));
|
2020-10-28 13:25:10 +05:00
|
|
|
}
|
|
|
|
|
default: {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-10-03 11:59:20 +05:00
|
|
|
}
|
2020-09-13 13:24:24 +05:00
|
|
|
}
|