mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 14:39:34 +01:00
inital work
This commit is contained in:
@@ -19,6 +19,7 @@ import UserManager from "./user-manager";
|
|||||||
import http from "../utils/http";
|
import http from "../utils/http";
|
||||||
import Monographs from "./monographs";
|
import Monographs from "./monographs";
|
||||||
import Offers from "./offers";
|
import Offers from "./offers";
|
||||||
|
import Attachments from "../collections/attachments";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {EventSource}
|
* @type {EventSource}
|
||||||
@@ -30,8 +31,9 @@ class Database {
|
|||||||
* @param {any} context
|
* @param {any} context
|
||||||
* @param {EventSource} eventsource
|
* @param {EventSource} eventsource
|
||||||
*/
|
*/
|
||||||
constructor(context, eventsource) {
|
constructor(context, eventsource, fs) {
|
||||||
this.context = new Storage(context);
|
this.context = new Storage(context);
|
||||||
|
this.fs = fs;
|
||||||
NNEventSource = eventsource;
|
NNEventSource = eventsource;
|
||||||
this._syncTimeout = 0;
|
this._syncTimeout = 0;
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,8 @@ class Database {
|
|||||||
this.colors = await Tags.new(this, "colors");
|
this.colors = await Tags.new(this, "colors");
|
||||||
/** @type {Content} */
|
/** @type {Content} */
|
||||||
this.content = await Content.new(this, "content", false);
|
this.content = await Content.new(this, "content", false);
|
||||||
|
/** @type {Attachments} */
|
||||||
|
this.attachments = await Attachments.new(this, "attachments");
|
||||||
|
|
||||||
this.trash = new Trash(this);
|
this.trash = new Trash(this);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { CURRENT_DATABASE_VERSION } from "../../common";
|
import { CURRENT_DATABASE_VERSION } from "../../common";
|
||||||
|
import { getContentFromData } from "../../content-types";
|
||||||
|
import { diff } from "../../utils/array";
|
||||||
import Database from "../index";
|
import Database from "../index";
|
||||||
|
|
||||||
class Collector {
|
class Collector {
|
||||||
@@ -8,9 +10,52 @@ class Collector {
|
|||||||
*/
|
*/
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
this._db = db;
|
this._db = db;
|
||||||
|
}
|
||||||
|
|
||||||
this._map = async (i) => {
|
async collect(lastSyncedTimestamp) {
|
||||||
const item = { ...i };
|
this._lastSyncedTimestamp = lastSyncedTimestamp;
|
||||||
|
this.key = await this._db.user.getEncryptionKey();
|
||||||
|
|
||||||
|
const contents = await this._db.content.extractAttachments(
|
||||||
|
this._collect(await this._db.content.all())
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
notes: await this._encrypt(this._collect(this._db.notes.raw)),
|
||||||
|
notebooks: await this._encrypt(this._collect(this._db.notebooks.raw)),
|
||||||
|
content: await this._encrypt(contents),
|
||||||
|
attachments: await this._encrypt(this._collect(this._db.attachments.all)),
|
||||||
|
settings: await this._encrypt(this._collect([this._db.settings.raw])),
|
||||||
|
vaultKey: await this._serialize(await this._db.vault._getKey()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_serialize(item) {
|
||||||
|
if (!item) return;
|
||||||
|
return this._db.context.encrypt(this.key, JSON.stringify(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
_encrypt(array) {
|
||||||
|
if (!array.length) return [];
|
||||||
|
return Promise.all(array.map(this._map, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Array} array
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
_collect(array) {
|
||||||
|
if (!array.length) return [];
|
||||||
|
return array.reduce((prev, item) => {
|
||||||
|
if (!item || item.localOnly) return prev;
|
||||||
|
if (item.dateEdited > this._lastSyncedTimestamp || item.migrated)
|
||||||
|
prev.push(item);
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _map(item) {
|
||||||
// in case of resolved content
|
// in case of resolved content
|
||||||
delete item.resolved;
|
delete item.resolved;
|
||||||
// turn the migrated flag off so we don't keep syncing this item repeated
|
// turn the migrated flag off so we don't keep syncing this item repeated
|
||||||
@@ -21,37 +66,6 @@ class Collector {
|
|||||||
v: CURRENT_DATABASE_VERSION,
|
v: CURRENT_DATABASE_VERSION,
|
||||||
...(await this._serialize(item)),
|
...(await this._serialize(item)),
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async collect(lastSyncedTimestamp) {
|
|
||||||
this._lastSyncedTimestamp = lastSyncedTimestamp;
|
|
||||||
this.key = await this._db.user.getEncryptionKey();
|
|
||||||
|
|
||||||
return {
|
|
||||||
notes: await this._collect(this._db.notes.raw),
|
|
||||||
notebooks: await this._collect(this._db.notebooks.raw),
|
|
||||||
content: await this._collect(await this._db.content.all()),
|
|
||||||
// trash: await this._collect(this._db.trash.raw),
|
|
||||||
settings: await this._collect([this._db.settings.raw]),
|
|
||||||
vaultKey: await this._serialize(await this._db.vault._getKey()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_serialize(item) {
|
|
||||||
if (!item) return;
|
|
||||||
return this._db.context.encrypt(this.key, JSON.stringify(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
_collect(array) {
|
|
||||||
return Promise.all(
|
|
||||||
array.reduce((prev, item) => {
|
|
||||||
if (!item || item.localOnly) return prev;
|
|
||||||
if (item.dateEdited > this._lastSyncedTimestamp || item.migrated)
|
|
||||||
prev.push(this._map(item));
|
|
||||||
return prev;
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default Collector;
|
export default Collector;
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export default class Sync {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this._isSyncing = false;
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
this._isSyncing = false;
|
this._isSyncing = false;
|
||||||
|
|||||||
@@ -207,7 +207,10 @@ export default class Vault {
|
|||||||
note = this._db.notes.note(id).data;
|
note = this._db.notes.note(id).data;
|
||||||
if (note.locked) return;
|
if (note.locked) return;
|
||||||
contentId = note.contentId;
|
contentId = note.contentId;
|
||||||
let content = await this._db.content.raw(contentId);
|
let content = await this._db.content.raw(contentId, false);
|
||||||
|
// TODO:
|
||||||
|
// 1. extract and empty out all attachments from note contents
|
||||||
|
// 2. encrypt them with vault password
|
||||||
data = content.data;
|
data = content.data;
|
||||||
type = content.type;
|
type = content.type;
|
||||||
}
|
}
|
||||||
|
|||||||
109
packages/core/collections/attachments.js
Normal file
109
packages/core/collections/attachments.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import Collection from "./collection";
|
||||||
|
import id from "../utils/id";
|
||||||
|
import SparkMD5 from "spark-md5";
|
||||||
|
import { deleteItem } from "../utils/array";
|
||||||
|
|
||||||
|
export default class Attachments extends Collection {
|
||||||
|
constructor(db, name, cached) {
|
||||||
|
super(db, name, cached);
|
||||||
|
this.key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initEncryptionKey() {
|
||||||
|
if (!this.key) this.key = await this._db.user.getEncryptionKey();
|
||||||
|
if (!this.key)
|
||||||
|
throw new Error(
|
||||||
|
"Failed to get user encryption key. Cannot cache attachments."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{
|
||||||
|
* iv: string,
|
||||||
|
* salt: string,
|
||||||
|
* length: number,
|
||||||
|
* alg: string,
|
||||||
|
* hash: string,
|
||||||
|
* hashType: string,
|
||||||
|
* filename: string,
|
||||||
|
* type: string
|
||||||
|
* }} attachment
|
||||||
|
* @param {string} noteId Optional as attachments will be parsed at extraction time
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
add(attachment, noteId) {
|
||||||
|
if (!attachment || !noteId)
|
||||||
|
return console.error("attachment or noteId cannot be null");
|
||||||
|
|
||||||
|
if (!attachment.hash) throw new Error("Please provide attachment hash.");
|
||||||
|
|
||||||
|
const oldAttachment = this.all.find(
|
||||||
|
(a) => a.metadata.hash === attachment.hash
|
||||||
|
);
|
||||||
|
if (oldAttachment) {
|
||||||
|
if (oldAttachment.noteIds.includes(noteId)) return;
|
||||||
|
|
||||||
|
oldAttachment.noteIds.push(noteId);
|
||||||
|
return this._collection.updateItem(oldAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { iv, salt, length, alg, hash, hashType, filename, type } =
|
||||||
|
attachment;
|
||||||
|
const attachmentItem = {
|
||||||
|
id: id(),
|
||||||
|
noteIds: noteId ? [noteId] : [],
|
||||||
|
iv,
|
||||||
|
salt,
|
||||||
|
length,
|
||||||
|
alg,
|
||||||
|
metadata: {
|
||||||
|
hash,
|
||||||
|
hashType,
|
||||||
|
filename,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
dateCreated: Date.now(),
|
||||||
|
dateEdited: undefined,
|
||||||
|
dateUploaded: undefined,
|
||||||
|
dateDeleted: undefined,
|
||||||
|
};
|
||||||
|
return this._collection.addItem(attachmentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(hash, noteId) {
|
||||||
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
||||||
|
if (!attachment || !deleteItem(attachment.noteIds, noteId)) return;
|
||||||
|
if (!attachment.noteIds.length) attachment.dateDeleted = Date.now();
|
||||||
|
|
||||||
|
return this._collection.updateItem(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(hash) {
|
||||||
|
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
||||||
|
if (!attachment) return;
|
||||||
|
await this._initEncryptionKey();
|
||||||
|
const data = await this._db.fs.readEncrypted(attachment.hash, this.key, {
|
||||||
|
iv: attachment.iv,
|
||||||
|
salt: attachment.salt,
|
||||||
|
length: attachment.length,
|
||||||
|
alg: attachment.alg,
|
||||||
|
outputType: "base64",
|
||||||
|
});
|
||||||
|
attachment.data = data;
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(data, type) {
|
||||||
|
await this._initEncryptionKey();
|
||||||
|
await this._db.fs.writeEncrypted(null, { data, type, key: this.key });
|
||||||
|
}
|
||||||
|
|
||||||
|
get pending() {
|
||||||
|
return this.all.filter((attachment) => !attachment.dateUploaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
get all() {
|
||||||
|
return this._collection.getItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import Collection from "./collection";
|
import Collection from "./collection";
|
||||||
import getId from "../utils/id";
|
import getId from "../utils/id";
|
||||||
|
import { getContentFromData } from "../content-types";
|
||||||
|
import { diff, hasItem } from "../utils/array";
|
||||||
|
|
||||||
export default class Content extends Collection {
|
export default class Content extends Collection {
|
||||||
async add(content) {
|
async add(content) {
|
||||||
@@ -37,10 +39,10 @@ export default class Content extends Collection {
|
|||||||
return content.data;
|
return content.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async raw(id) {
|
async raw(id, withAttachments = true) {
|
||||||
const content = await this._collection.getItem(id);
|
const content = await this._collection.getItem(id);
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
return content;
|
return withAttachments ? await this.insertAttachments(content) : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id) {
|
remove(id) {
|
||||||
@@ -56,22 +58,37 @@ export default class Content extends Collection {
|
|||||||
return this._collection.getItems(this._collection.indexer.indices);
|
return this._collection.getItems(this._collection.indexer.indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
async insertAttachments(contentItem) {
|
||||||
const indices = this._collection.indexer.indices;
|
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||||
await this._db.notes.init();
|
contentItem.data = await content.insertAttachments((hash) => {
|
||||||
const notes = this._db.notes._collection.getRaw();
|
return this._db.attachments.get(hash);
|
||||||
if (!notes.length && indices.length > 0) return [];
|
});
|
||||||
let ids = [];
|
return contentItem;
|
||||||
for (let contentId of indices) {
|
}
|
||||||
const noteIndex = notes.findIndex((note) => note.contentId === contentId);
|
|
||||||
const isOrphaned = noteIndex === -1;
|
async extractAttachments(contents) {
|
||||||
if (isOrphaned) {
|
const allAttachments = this._db.attachments.all;
|
||||||
ids.push(contentId);
|
for (let contentItem of contents) {
|
||||||
await this._collection.deleteItem(contentId);
|
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||||
} else if (notes[noteIndex].localOnly) {
|
const { data, attachments } = await content.extractAttachments(
|
||||||
ids.push(contentId);
|
(data, type) => this._db.attachments.save(data, type)
|
||||||
}
|
);
|
||||||
}
|
|
||||||
return ids;
|
await diff(allAttachments, attachments, async (attachment, action) => {
|
||||||
|
if (hasItem(attachment.noteIds, contentItem.noteId)) return;
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
await this._db.attachments.delete(
|
||||||
|
attachment.hash,
|
||||||
|
contentItem.noteId
|
||||||
|
);
|
||||||
|
} else if (action === "insert") {
|
||||||
|
await this._db.attachments.add(attachment, contentItem.noteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
contentItem.data = data;
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import showdown from "showdown";
|
import showdown from "showdown";
|
||||||
import decode from "lean-he/decode";
|
import decode from "lean-he/decode";
|
||||||
|
import SparkMD5 from "spark-md5";
|
||||||
|
import dataurl from "../utils/dataurl";
|
||||||
|
|
||||||
var converter = new showdown.Converter();
|
var converter = new showdown.Converter();
|
||||||
converter.setFlavor("original");
|
converter.setFlavor("original");
|
||||||
@@ -56,6 +58,80 @@ class Tiny {
|
|||||||
const lowercase = this.toTXT().toLowerCase();
|
const lowercase = this.toTXT().toLowerCase();
|
||||||
return tokens.some((token) => lowercase.indexOf(token) > -1);
|
return tokens.some((token) => lowercase.indexOf(token) > -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async insertAttachments(get) {
|
||||||
|
if (!("DOMParser" in window || "DOMParser" in global)) return;
|
||||||
|
|
||||||
|
let doc = new DOMParser().parseFromString(this.data, "text/html");
|
||||||
|
const attachmentElements = doc.querySelectorAll("img");
|
||||||
|
|
||||||
|
for (let attachment of attachmentElements) {
|
||||||
|
switch (attachment.tagName) {
|
||||||
|
case "IMG": {
|
||||||
|
const hash = attachment.dataset["hash"];
|
||||||
|
|
||||||
|
const attachmentItem = await get(hash);
|
||||||
|
if (!attachmentItem) continue;
|
||||||
|
|
||||||
|
attachment.setAttribute(
|
||||||
|
"src",
|
||||||
|
dataurl.fromObject({
|
||||||
|
data: attachmentItem.data,
|
||||||
|
type: attachmentItem.metadata.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async extractAttachments(store) {
|
||||||
|
if (!("DOMParser" in window || "DOMParser" in global)) return;
|
||||||
|
|
||||||
|
let doc = new DOMParser().parseFromString(this.data, "text/html");
|
||||||
|
const attachmentElements = doc.querySelectorAll("img,.attachment");
|
||||||
|
|
||||||
|
const attachments = [];
|
||||||
|
for (let attachment of attachmentElements) {
|
||||||
|
switch (attachment.tagName) {
|
||||||
|
case "IMG": {
|
||||||
|
if (!attachment.dataset.hash) {
|
||||||
|
const src = attachment.getAttribute("src");
|
||||||
|
if (!src) continue;
|
||||||
|
|
||||||
|
const { data, mime } = dataurl.toObject(src);
|
||||||
|
if (!data) continue;
|
||||||
|
|
||||||
|
const type = attachment.dataset.mime || mime || "image/jpeg";
|
||||||
|
const metadata = await store(data, "base64");
|
||||||
|
attachment.dataset.hash = metadata.hash;
|
||||||
|
|
||||||
|
attachments.push({
|
||||||
|
type,
|
||||||
|
filename: attachment.dataset.filename,
|
||||||
|
...metadata,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
attachments.push({
|
||||||
|
hash: attachment.dataset.hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
attachment.removeAttribute("src");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (!attachment.dataset.hash) return;
|
||||||
|
attachments.push({
|
||||||
|
hash: attachment.dataset.hash,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data: doc.body.innerHTML, attachments };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default Tiny;
|
export default Tiny;
|
||||||
|
|
||||||
|
|||||||
14839
packages/core/package-lock.json
generated
Normal file
14839
packages/core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,13 +20,30 @@ export function deleteItems(array, ...items) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findById(array, id) {
|
export function findById(array, id) {
|
||||||
|
if (!array) return false;
|
||||||
return array.find((item) => item.id === id);
|
return array.find((item) => item.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasItem(array, item) {
|
export function hasItem(array, item) {
|
||||||
|
if (!array) return false;
|
||||||
return array.indexOf(item) > -1;
|
return array.indexOf(item) > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function diff(arr1, arr2, action) {
|
||||||
|
let length = arr1.length + arr2.length;
|
||||||
|
for (var i = 0; i < length; ++i) {
|
||||||
|
var actionKey = "delete";
|
||||||
|
var item = arr1[i];
|
||||||
|
|
||||||
|
if (i >= arr1.length) {
|
||||||
|
var actionKey = "insert";
|
||||||
|
var item = arr2[i - arr1.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
await action(item, actionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteAtIndex(array, index) {
|
function deleteAtIndex(array, index) {
|
||||||
if (index === -1) return false;
|
if (index === -1) return false;
|
||||||
array.splice(index, 1);
|
array.splice(index, 1);
|
||||||
|
|||||||
14
packages/core/utils/dataurl.js
Normal file
14
packages/core/utils/dataurl.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const REGEX = /^data:(?<mime>image\/.+);base64,(?<data>.+)/;
|
||||||
|
|
||||||
|
function toObject(dataurl) {
|
||||||
|
const { groups } = REGEX.exec(dataurl);
|
||||||
|
return groups || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromObject({ type, data }) {
|
||||||
|
//const { groups } = REGEX.exec(dataurl);
|
||||||
|
return `data:${type};base64,${data}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataurl = { toObject, fromObject };
|
||||||
|
export default dataurl;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user