mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
feat: implement backup migration
This commit is contained in:
1
packages/core/__tests__/__fixtures__/backup.v0.json
Normal file
1
packages/core/__tests__/__fixtures__/backup.v0.json
Normal file
File diff suppressed because one or more lines are too long
@@ -2,13 +2,9 @@ import {
|
|||||||
StorageInterface,
|
StorageInterface,
|
||||||
databaseTest,
|
databaseTest,
|
||||||
noteTest,
|
noteTest,
|
||||||
groupedTest,
|
|
||||||
LONG_TEXT,
|
|
||||||
TEST_NOTE,
|
|
||||||
TEST_NOTEBOOK,
|
|
||||||
notebookTest,
|
notebookTest,
|
||||||
TEST_NOTEBOOK2,
|
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import v0Backup from "./__fixtures__/backup.v0.json";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
StorageInterface.clear();
|
StorageInterface.clear();
|
||||||
@@ -67,3 +63,35 @@ test("import tempered backup", () =>
|
|||||||
).rejects.toThrowError(/tempered/);
|
).rejects.toThrowError(/tempered/);
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
|
|
||||||
|
test.only("import unversioned (v0) backup", () => {
|
||||||
|
return databaseTest().then(async (db) => {
|
||||||
|
await db.backup.import(JSON.stringify(v0Backup));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
db.notes.all.every(
|
||||||
|
(v) => v.contentId && !v.content && (!v.notebook || !v.notebook.id)
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
db.notebooks.all.every((v) => v.title != null && v.description != null)
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
function verifyIndex(db, backupCollection, collection) {
|
||||||
|
if (!v0Backup.data[backupCollection]) return;
|
||||||
|
expect(
|
||||||
|
v0Backup.data[backupCollection].every(
|
||||||
|
(v) => db[collection]._collection.indexer.indices.indexOf(v) > -1
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyIndex(db, "notes", "notes");
|
||||||
|
verifyIndex(db, "notebooks", "notebooks");
|
||||||
|
verifyIndex(db, "delta", "content");
|
||||||
|
verifyIndex(db, "tags", "tags");
|
||||||
|
verifyIndex(db, "colors", "colors");
|
||||||
|
verifyIndex(db, "trash", "trash");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const md5 = new Hashes.MD5();
|
|||||||
|
|
||||||
const invalidKeys = ["user", "t", "lastBackupTime"];
|
const invalidKeys = ["user", "t", "lastBackupTime"];
|
||||||
const validTypes = ["mobile", "web", "node"];
|
const validTypes = ["mobile", "web", "node"];
|
||||||
|
const CURRENT_BACKUP_VERSION = 2;
|
||||||
export default class Backup {
|
export default class Backup {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -43,7 +44,7 @@ export default class Backup {
|
|||||||
await this._db.context.write("lastBackupTime", Date.now());
|
await this._db.context.write("lastBackupTime", Date.now());
|
||||||
|
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
version: 2,
|
version: CURRENT_BACKUP_VERSION,
|
||||||
type,
|
type,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
data,
|
data,
|
||||||
@@ -63,6 +64,8 @@ export default class Backup {
|
|||||||
|
|
||||||
if (!this._validate(backup)) throw new Error("Invalid backup.");
|
if (!this._validate(backup)) throw new Error("Invalid backup.");
|
||||||
|
|
||||||
|
backup = this._migrateBackup(backup);
|
||||||
|
|
||||||
let db = backup.data;
|
let db = backup.data;
|
||||||
//check if we have encrypted data
|
//check if we have encrypted data
|
||||||
if (db.salt && db.iv) {
|
if (db.salt && db.iv) {
|
||||||
@@ -72,24 +75,71 @@ export default class Backup {
|
|||||||
|
|
||||||
if (!this._verify(backup))
|
if (!this._verify(backup))
|
||||||
throw new Error("Backup file has been tempered, aborting...");
|
throw new Error("Backup file has been tempered, aborting...");
|
||||||
// TODO add a proper restoration system.
|
|
||||||
// for (let key in db) {
|
|
||||||
// let value = db[key];
|
|
||||||
// if (value && value.dateEdited) {
|
|
||||||
// value.dateEdited = Date.now();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const oldValue = await this._db.context.read(oldValue);
|
await this._migrateData(backup);
|
||||||
|
}
|
||||||
|
|
||||||
// let finalValue = oldValue || value;
|
_migrateBackup(backup) {
|
||||||
// if (typeof value === "object") {
|
const { version = 0 } = backup;
|
||||||
// finalValue = Array.isArray(value)
|
if (version > CURRENT_BACKUP_VERSION)
|
||||||
// ? [...value, ...oldValue]
|
throw new Error(
|
||||||
// : { ...value, ...oldValue };
|
"This backup was made from a newer version of Notesnook. Cannot migrate."
|
||||||
// }
|
);
|
||||||
|
|
||||||
// await this._db.context.write(key, finalValue);
|
switch (version) {
|
||||||
// }
|
case CURRENT_BACKUP_VERSION: {
|
||||||
|
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;
|
||||||
|
if (version > CURRENT_BACKUP_VERSION)
|
||||||
|
throw new Error(
|
||||||
|
"This backup was made from a newer version of Notesnook. Cannot migrate."
|
||||||
|
);
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
"notes",
|
||||||
|
"notebooks",
|
||||||
|
"tags",
|
||||||
|
"colors",
|
||||||
|
"trash",
|
||||||
|
"delta",
|
||||||
|
"text",
|
||||||
|
"content",
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
collections.map(async (collection) => {
|
||||||
|
const collectionIndex = data[collection];
|
||||||
|
if (!collectionIndex) return;
|
||||||
|
await Promise.all(
|
||||||
|
collectionIndex.map(async (id) => {
|
||||||
|
const item = data[id];
|
||||||
|
if (!item) return;
|
||||||
|
await migrations[version][collection](this._db, item);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_validate(backup) {
|
_validate(backup) {
|
||||||
@@ -113,3 +163,82 @@ export default class Backup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const migrations = {
|
||||||
|
handleDeleted: async function (db, collection, item) {
|
||||||
|
if (item.deleted) {
|
||||||
|
await db[collection]._collection.addItem(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
0: {
|
||||||
|
notes: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "notes", item)) return;
|
||||||
|
|
||||||
|
const contentId = item.content.delta;
|
||||||
|
delete item.content;
|
||||||
|
item.contentId = contentId;
|
||||||
|
item.remote = true;
|
||||||
|
if (!item.notebook.id) item.notebook = undefined;
|
||||||
|
await db.notes.add(item);
|
||||||
|
},
|
||||||
|
delta: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "content", item)) return;
|
||||||
|
|
||||||
|
item.data = item.data.ops;
|
||||||
|
item.type = "delta";
|
||||||
|
await db.content.add(item);
|
||||||
|
},
|
||||||
|
trash: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "trash", item)) return;
|
||||||
|
|
||||||
|
item.itemType = item.type;
|
||||||
|
item.type = "trash";
|
||||||
|
if (item.itemType === "note") {
|
||||||
|
item.contentId = item.content.delta;
|
||||||
|
delete item.content;
|
||||||
|
}
|
||||||
|
await db.trash.add(item);
|
||||||
|
},
|
||||||
|
notebooks: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "notebooks", item)) return;
|
||||||
|
await db.notebooks.add(item);
|
||||||
|
},
|
||||||
|
tags: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "tags", item)) return;
|
||||||
|
await db.tags.add(item);
|
||||||
|
},
|
||||||
|
colors: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "colors", item)) return;
|
||||||
|
await db.tags.add(item);
|
||||||
|
},
|
||||||
|
text: function () {},
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
notes: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "notes", item)) return;
|
||||||
|
await db.notes.add({ ...item, remote: true });
|
||||||
|
},
|
||||||
|
notebooks: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "notebooks", item)) return;
|
||||||
|
await db.notebooks.add(item);
|
||||||
|
},
|
||||||
|
tags: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "tags", item)) return;
|
||||||
|
await db.tags.add(item);
|
||||||
|
},
|
||||||
|
colors: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "colors", item)) return;
|
||||||
|
await db.tags.add(item);
|
||||||
|
},
|
||||||
|
trash: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "trash", item)) return;
|
||||||
|
await db.trash.add(item);
|
||||||
|
},
|
||||||
|
content: async function (db, item) {
|
||||||
|
if (await migrations.handleDeleted(db, "content", item)) return;
|
||||||
|
await db.content.add(item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user