mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-24 07:29:30 +01:00
feat: refactors and add support for upload/download progress
This commit is contained in:
@@ -7,7 +7,7 @@ beforeEach(async () => {
|
||||
test("create vault", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
await expect(db.vault.create("password")).resolves.toBe(true);
|
||||
const vaultKey = await db.context.read("vaultKey");
|
||||
const vaultKey = await db.storage.read("vaultKey");
|
||||
expect(vaultKey).toBeDefined();
|
||||
expect(vaultKey.iv).toBeDefined();
|
||||
expect(vaultKey.cipher).toBeDefined();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Notes from "../collections/notes";
|
||||
import Storage from "../database/storage";
|
||||
import FileStorage from "../database/fs";
|
||||
import Notebooks from "../collections/notebooks";
|
||||
import Trash from "../collections/trash";
|
||||
import Tags from "../collections/tags";
|
||||
@@ -28,12 +29,12 @@ var NNEventSource;
|
||||
class Database {
|
||||
/**
|
||||
*
|
||||
* @param {any} context
|
||||
* @param {any} storage
|
||||
* @param {EventSource} eventsource
|
||||
*/
|
||||
constructor(context, eventsource, fs) {
|
||||
this.context = new Storage(context);
|
||||
this.fs = fs;
|
||||
constructor(storage, eventsource, fs) {
|
||||
this.storage = new Storage(storage);
|
||||
this.fs = new FileStorage(fs, storage);
|
||||
NNEventSource = eventsource;
|
||||
this._syncTimeout = 0;
|
||||
}
|
||||
@@ -62,10 +63,10 @@ class Database {
|
||||
});
|
||||
EV.subscribe(EVENTS.databaseUpdated, this._onDBWrite.bind(this));
|
||||
|
||||
this.session = new Session(this.context);
|
||||
this.session = new Session(this.storage);
|
||||
await this._validate();
|
||||
|
||||
this.user = new UserManager(this);
|
||||
this.user = new UserManager(this.storage);
|
||||
this.syncer = new Sync(this);
|
||||
this.vault = new Vault(this);
|
||||
this.conflicts = new Conflicts(this);
|
||||
@@ -152,7 +153,7 @@ class Database {
|
||||
await this.user.logout(true, "Password Changed");
|
||||
break;
|
||||
case "emailConfirmed":
|
||||
const token = await this.context.read("token");
|
||||
const token = await this.storage.read("token");
|
||||
await this.user.tokenManager._refreshToken(token);
|
||||
await this.user.fetchUser(true);
|
||||
EV.publish(EVENTS.userEmailConfirmed);
|
||||
@@ -166,7 +167,7 @@ class Database {
|
||||
}
|
||||
|
||||
async lastSynced() {
|
||||
return this.context.read("lastSynced");
|
||||
return this.storage.read("lastSynced");
|
||||
}
|
||||
|
||||
async _onDBWrite(item) {
|
||||
|
||||
@@ -13,8 +13,8 @@ class Migrations {
|
||||
|
||||
async init() {
|
||||
this.dbVersion =
|
||||
(await this._db.context.read("v")) || CURRENT_DATABASE_VERSION;
|
||||
this._db.context.write("v", this.dbVersion);
|
||||
(await this._db.storage.read("v")) || CURRENT_DATABASE_VERSION;
|
||||
this._db.storage.write("v", this.dbVersion);
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
@@ -55,7 +55,7 @@ class Migrations {
|
||||
},
|
||||
];
|
||||
await this._migrator.migrate(collections, (item) => item, this.dbVersion);
|
||||
await this._db.context.write("v", CURRENT_DATABASE_VERSION);
|
||||
await this._db.storage.write("v", CURRENT_DATABASE_VERSION);
|
||||
|
||||
EV.publish(EVENTS.databaseMigrated, {
|
||||
prev: this.dbVersion,
|
||||
|
||||
@@ -13,17 +13,17 @@ class Monographs {
|
||||
|
||||
async deinit() {
|
||||
this.monographs = [];
|
||||
await this._db.context.write("monographs", this.monographs);
|
||||
await this._db.storage.write("monographs", this.monographs);
|
||||
}
|
||||
|
||||
async init() {
|
||||
const user = await this._db.user.getUser();
|
||||
const token = await this._db.user.tokenManager.getAccessToken();
|
||||
if (!user || !token || !user.isEmailConfirmed) return;
|
||||
let monographs = await this._db.context.read("monographs", true);
|
||||
let monographs = await this._db.storage.read("monographs", true);
|
||||
try {
|
||||
monographs = await http.get(`${Constants.API_HOST}/monographs`, token);
|
||||
await this._db.context.write("monographs", monographs);
|
||||
await this._db.storage.write("monographs", monographs);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class Monographs {
|
||||
};
|
||||
|
||||
if (opts.password) {
|
||||
monograph.encryptedContent = await this._db.context.encrypt(
|
||||
monograph.encryptedContent = await this._db.storage.encrypt(
|
||||
{ password: opts.password },
|
||||
JSON.stringify({ type: content.type, data: content.data })
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ class Outbox {
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.outbox = (await this._db.context.read("outbox")) || {};
|
||||
this.outbox = (await this._db.storage.read("outbox")) || {};
|
||||
|
||||
for (var id in this.outbox) {
|
||||
const data = this.outbox[id];
|
||||
@@ -18,7 +18,7 @@ class Outbox {
|
||||
case "change_password":
|
||||
const key = await this._db.user.getEncryptionKey();
|
||||
const { email } = await this._db.user.getUser();
|
||||
await this._db.context.deriveCryptoKey(`_uk_@${email}`, {
|
||||
await this._db.storage.deriveCryptoKey(`_uk_@${email}`, {
|
||||
password: data.newPassword,
|
||||
salt: key.salt,
|
||||
});
|
||||
@@ -31,14 +31,14 @@ class Outbox {
|
||||
|
||||
async add(id, data, action) {
|
||||
this.outbox[id] = data;
|
||||
await this._db.context.write("outbox", this.outbox);
|
||||
await this._db.storage.write("outbox", this.outbox);
|
||||
await action();
|
||||
await this.delete(id);
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
delete this.outbox[id];
|
||||
return this._db.context.write("outbox", this.outbox);
|
||||
return this._db.storage.write("outbox", this.outbox);
|
||||
}
|
||||
}
|
||||
export default Outbox;
|
||||
|
||||
@@ -4,15 +4,15 @@ class Session {
|
||||
* @param {import("../database/storage").default} context
|
||||
*/
|
||||
constructor(context) {
|
||||
this._context = context;
|
||||
this._storage = context;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this._context.read("t");
|
||||
return this._storage.read("t");
|
||||
}
|
||||
|
||||
set() {
|
||||
return this._context.write("t", Date.now());
|
||||
return this._storage.write("t", Date.now());
|
||||
}
|
||||
|
||||
async valid() {
|
||||
|
||||
@@ -14,7 +14,7 @@ class Settings {
|
||||
}
|
||||
|
||||
async init() {
|
||||
var settings = await this._db.context.read("settings");
|
||||
var settings = await this._db.storage.read("settings");
|
||||
this._initSettings(settings);
|
||||
await this._saveSettings();
|
||||
|
||||
@@ -138,7 +138,7 @@ class Settings {
|
||||
}
|
||||
|
||||
async _saveSettings() {
|
||||
await this._db.context.write("settings", this._settings);
|
||||
await this._db.storage.write("settings", this._settings);
|
||||
}
|
||||
}
|
||||
export default Settings;
|
||||
|
||||
@@ -32,7 +32,7 @@ class Collector {
|
||||
|
||||
_serialize(item) {
|
||||
if (!item) return null;
|
||||
return this._db.context.encrypt(this.key, JSON.stringify(item));
|
||||
return this._db.storage.encrypt(this.key, JSON.stringify(item));
|
||||
}
|
||||
|
||||
_encrypt(array) {
|
||||
|
||||
@@ -9,12 +9,12 @@ class Conflicts {
|
||||
|
||||
async recalculate() {
|
||||
if (this._db.notes.conflicted.length <= 0) {
|
||||
await this._db.context.write("hasConflicts", false);
|
||||
await this._db.storage.write("hasConflicts", false);
|
||||
}
|
||||
}
|
||||
|
||||
async check() {
|
||||
let hasConflicts = await this._db.context.read("hasConflicts");
|
||||
let hasConflicts = await this._db.storage.read("hasConflicts");
|
||||
if (hasConflicts) {
|
||||
const mergeConflictError = new Error(
|
||||
"Merge conflicts detected. Please resolve all conflicts to continue syncing."
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class Sync {
|
||||
this._db = db;
|
||||
this._collector = new Collector(this._db);
|
||||
this._merger = new Merger(this._db);
|
||||
this._tokenManager = new TokenManager(this._db);
|
||||
this._tokenManager = new TokenManager(this._db.storage);
|
||||
this._isSyncing = false;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class Sync {
|
||||
}
|
||||
|
||||
async _performChecks() {
|
||||
let lastSynced = (await this._db.context.read("lastSynced")) || 0;
|
||||
let lastSynced = (await this._db.storage.read("lastSynced")) || 0;
|
||||
let token = await this._tokenManager.getAccessToken();
|
||||
|
||||
// update the conflicts status and if find any, throw
|
||||
@@ -66,7 +66,7 @@ export default class Sync {
|
||||
async start(full, force) {
|
||||
if (this._isSyncing) return false;
|
||||
|
||||
if (force) await this._db.context.write("lastSynced", 0);
|
||||
if (force) await this._db.storage.write("lastSynced", 0);
|
||||
let { lastSynced, token } = await this._performChecks();
|
||||
|
||||
try {
|
||||
@@ -84,7 +84,7 @@ export default class Sync {
|
||||
// merge the server response
|
||||
await this._merger.merge(serverResponse, lastSynced);
|
||||
|
||||
await this._downloadAttachments(token);
|
||||
// await this._downloadAttachments(token);
|
||||
}
|
||||
|
||||
// check for conflicts and throw
|
||||
@@ -95,7 +95,7 @@ export default class Sync {
|
||||
|
||||
// update our lastSynced time
|
||||
if (lastSynced) {
|
||||
await this._db.context.write("lastSynced", lastSynced);
|
||||
await this._db.storage.write("lastSynced", lastSynced);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -123,7 +123,7 @@ export default class Sync {
|
||||
|
||||
// update our lastSynced time
|
||||
if (lastSynced) {
|
||||
await this._db.context.write("lastSynced", lastSynced);
|
||||
await this._db.storage.write("lastSynced", lastSynced);
|
||||
}
|
||||
|
||||
EV.publish(EVENTS.appRefreshRequested);
|
||||
@@ -137,7 +137,7 @@ export default class Sync {
|
||||
// last edited (but unsynced) time resulting in edited notes
|
||||
// not getting synced.
|
||||
// if (serverResponse.lastSynced) {
|
||||
// await this._db.context.write("lastSynced", serverResponse.lastSynced);
|
||||
// await this._db.storage.write("lastSynced", serverResponse.lastSynced);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -154,37 +154,24 @@ export default class Sync {
|
||||
async _uploadAttachments(token) {
|
||||
const attachments = this._db.attachments.pending;
|
||||
console.log("Uploading attachments", this._db.attachments.pending);
|
||||
for (let attachment of attachments) {
|
||||
for (var i = 0; i < attachments.length; ++i) {
|
||||
const attachment = attachments[i];
|
||||
EV.publish(EVENTS.attachmentsLoading, {
|
||||
type: "upload",
|
||||
total: attachments.length,
|
||||
current: i,
|
||||
});
|
||||
const { hash } = attachment.metadata;
|
||||
const url = await this._getPresignedURL(hash, token, "PUT");
|
||||
const uploadResult = await this._db.fs.uploadFile(hash, { url });
|
||||
if (!uploadResult) throw new Error("Failed to upload file.");
|
||||
|
||||
const isUploaded = await this._db.fs.uploadFile(hash);
|
||||
if (!isUploaded) throw new Error("Failed to upload file.");
|
||||
|
||||
await this._db.attachments.markAsUploaded(attachment.id);
|
||||
}
|
||||
}
|
||||
|
||||
async _downloadAttachments(token) {
|
||||
const attachments = this._db.attachments.media;
|
||||
console.log("Downloading attachments", attachments);
|
||||
for (let attachment of attachments) {
|
||||
const { hash } = attachment.metadata;
|
||||
const url = `${Constants.API_HOST}/s3?name=${hash}`;
|
||||
const downloadResult = await this._db.fs.downloadFile(hash, {
|
||||
url,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!downloadResult) throw new Error("Failed to download file.");
|
||||
}
|
||||
}
|
||||
|
||||
async _getPresignedURL(filename, token, verb) {
|
||||
const response = await fetch(`${Constants.API_HOST}/s3?name=${filename}`, {
|
||||
method: verb,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
EV.publish(EVENTS.attachmentsLoading, {
|
||||
type: "upload",
|
||||
total: attachments.length,
|
||||
current: attachments.length,
|
||||
});
|
||||
if (response.ok) return await response.text();
|
||||
throw new Error("Couldn't get presigned url.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class Merger {
|
||||
|
||||
async _deserialize(item, migrate = true) {
|
||||
const deserialized = JSON.parse(
|
||||
await this._db.context.decrypt(this.key, item)
|
||||
await this._db.storage.decrypt(this.key, item)
|
||||
);
|
||||
deserialized.remote = true;
|
||||
if (!migrate) return deserialized;
|
||||
@@ -147,7 +147,7 @@ class Merger {
|
||||
// otherwise we trigger the conflicts
|
||||
await this._db.content.add({ ...local, conflicted: remote });
|
||||
await this._db.notes.add({ id: local.noteId, conflicted: true });
|
||||
await this._db.context.write("hasConflicts", true);
|
||||
await this._db.storage.write("hasConflicts", true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -13,14 +13,14 @@ var isRefreshingToken = false;
|
||||
class TokenManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("./index").default} db
|
||||
* @param {import("../database/storage").default} storage
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
constructor(storage) {
|
||||
this._storage = storage;
|
||||
}
|
||||
|
||||
async getToken(renew = true, forceRenew = false) {
|
||||
let token = await this._db.context.read("token");
|
||||
let token = await this._storage.read("token");
|
||||
if (!token) return;
|
||||
if (forceRenew || (renew && this._isTokenExpired(token))) {
|
||||
await this._refreshToken(token);
|
||||
@@ -107,7 +107,7 @@ class TokenManager {
|
||||
|
||||
saveToken(tokenResponse) {
|
||||
let token = { ...tokenResponse, t: Date.now() };
|
||||
return this._db.context.write("token", token);
|
||||
return this._storage.write("token", token);
|
||||
}
|
||||
|
||||
async getAccessTokenFromAuthorizationCode(userId, authCode) {
|
||||
|
||||
@@ -22,11 +22,11 @@ const ENDPOINTS = {
|
||||
class UserManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("./index").default} db
|
||||
* @param {import("../database/storage").default} storage
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this.tokenManager = new TokenManager(db);
|
||||
constructor(storage) {
|
||||
this._storage = storage;
|
||||
this.tokenManager = new TokenManager(storage);
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -36,7 +36,7 @@ class UserManager {
|
||||
}
|
||||
|
||||
async signup(email, password) {
|
||||
const hashedPassword = await this._db.context.hash(password, email);
|
||||
const hashedPassword = await this._storage.hash(password, email);
|
||||
await http.post(`${constants.API_HOST}${ENDPOINTS.signup}`, {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
@@ -48,7 +48,7 @@ class UserManager {
|
||||
|
||||
async login(email, password, hashedPassword) {
|
||||
if (!hashedPassword) {
|
||||
hashedPassword = await this._db.context.hash(password, email);
|
||||
hashedPassword = await this._storage.hash(password, email);
|
||||
}
|
||||
|
||||
await this.tokenManager.saveToken(
|
||||
@@ -63,7 +63,7 @@ class UserManager {
|
||||
|
||||
const user = await this.fetchUser();
|
||||
setUserPersonalizationBytes(user.salt);
|
||||
await this._db.context.deriveCryptoKey(`_uk_@${user.email}`, {
|
||||
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
|
||||
password,
|
||||
salt: user.salt,
|
||||
});
|
||||
@@ -83,7 +83,7 @@ class UserManager {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
await this._db.context.clear();
|
||||
await this._storage.clear();
|
||||
EV.publish(EVENTS.userLoggedOut, reason);
|
||||
EV.publish(EVENTS.appRefreshRequested);
|
||||
}
|
||||
@@ -91,11 +91,11 @@ class UserManager {
|
||||
|
||||
setUser(user) {
|
||||
if (!user) return;
|
||||
return this._db.context.write("user", user);
|
||||
return this._storage.write("user", user);
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this._db.context.read("user");
|
||||
return this._storage.read("user");
|
||||
}
|
||||
|
||||
async deleteUser(password) {
|
||||
@@ -104,7 +104,7 @@ class UserManager {
|
||||
const user = await this.getUser();
|
||||
await http.post(
|
||||
`${constants.API_HOST}${ENDPOINTS.deleteUser}`,
|
||||
{ password: await this._db.context.hash(password, user.email) },
|
||||
{ password: await this._storage.hash(password, user.email) },
|
||||
token
|
||||
);
|
||||
await this.logout(false, "Account deleted.");
|
||||
@@ -155,7 +155,7 @@ class UserManager {
|
||||
async getEncryptionKey() {
|
||||
const user = await this.getUser();
|
||||
if (!user) return;
|
||||
const key = await this._db.context.getCryptoKey(`_uk_@${user.email}`);
|
||||
const key = await this._storage.getCryptoKey(`_uk_@${user.email}`);
|
||||
return { key, salt: user.salt };
|
||||
}
|
||||
|
||||
@@ -181,8 +181,8 @@ class UserManager {
|
||||
const user = await this.getUser();
|
||||
if (!user) return false;
|
||||
const key = await this.getEncryptionKey();
|
||||
const cipher = await this._db.context.encrypt(key, "notesnook");
|
||||
const plainText = await this._db.context.decrypt({ password }, cipher);
|
||||
const cipher = await this._storage.encrypt(key, "notesnook");
|
||||
const plainText = await this._storage.decrypt({ password }, cipher);
|
||||
return plainText === "notesnook";
|
||||
} catch (e) {
|
||||
return false;
|
||||
@@ -197,12 +197,12 @@ class UserManager {
|
||||
const { email, salt } = await this.getUser();
|
||||
var hashedData = {};
|
||||
if (data.old_password)
|
||||
hashedData.old_password = await this._db.context.hash(
|
||||
hashedData.old_password = await this._storage.hash(
|
||||
data.old_password,
|
||||
email
|
||||
);
|
||||
if (data.new_password)
|
||||
hashedData.new_password = await this._db.context.hash(
|
||||
hashedData.new_password = await this._storage.hash(
|
||||
data.new_password,
|
||||
email
|
||||
);
|
||||
@@ -215,20 +215,21 @@ class UserManager {
|
||||
},
|
||||
token
|
||||
);
|
||||
await this._db.outbox.add(
|
||||
type,
|
||||
{ newPassword: data.new_password },
|
||||
async () => {
|
||||
await this._db.sync(true);
|
||||
// TODO
|
||||
// await this._db.outbox.add(
|
||||
// type,
|
||||
// { newPassword: data.new_password },
|
||||
// async () => {
|
||||
// await this._db.sync(true);
|
||||
|
||||
await this._db.context.deriveCryptoKey(`_uk_@${email}`, {
|
||||
password: data.new_password,
|
||||
salt,
|
||||
});
|
||||
// await this._storage.deriveCryptoKey(`_uk_@${email}`, {
|
||||
// password: data.new_password,
|
||||
// salt,
|
||||
// });
|
||||
|
||||
await this._db.sync(false, true);
|
||||
}
|
||||
);
|
||||
// await this._db.sync(false, true);
|
||||
// }
|
||||
// );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class Vault {
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this._context = db.context;
|
||||
this._storage = db.storage;
|
||||
this._key = "svvaads1212#2123";
|
||||
this._vaultPassword = null;
|
||||
this.ERRORS = {
|
||||
@@ -48,13 +48,13 @@ export default class Vault {
|
||||
async create(password) {
|
||||
if (!(await sendCheckUserStatusEvent(CHECK_IDS.vaultAdd))) return;
|
||||
|
||||
const vaultKey = await this._context.read("vaultKey");
|
||||
const vaultKey = await this._storage.read("vaultKey");
|
||||
if (!vaultKey || !vaultKey.cipher || !vaultKey.iv) {
|
||||
const encryptedData = await this._context.encrypt(
|
||||
const encryptedData = await this._storage.encrypt(
|
||||
{ password },
|
||||
this._key
|
||||
);
|
||||
await this._context.write("vaultKey", encryptedData);
|
||||
await this._storage.write("vaultKey", encryptedData);
|
||||
this._password = password;
|
||||
}
|
||||
return true;
|
||||
@@ -67,10 +67,10 @@ export default class Vault {
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async unlock(password) {
|
||||
const vaultKey = await this._context.read("vaultKey");
|
||||
const vaultKey = await this._storage.read("vaultKey");
|
||||
if (!(await this.exists(vaultKey))) throw new Error(this.ERRORS.noVault);
|
||||
try {
|
||||
await this._context.decrypt({ password }, vaultKey);
|
||||
await this._storage.decrypt({ password }, vaultKey);
|
||||
} catch (e) {
|
||||
throw new Error(this.ERRORS.wrongPassword);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export default class Vault {
|
||||
for (var note of lockedNotes) {
|
||||
await this._unlockNote(note, oldPassword, true);
|
||||
}
|
||||
await this._context.remove("vaultKey");
|
||||
await this._storage.remove("vaultKey");
|
||||
await this.create(newPassword);
|
||||
for (var note of lockedNotes) {
|
||||
await this._lockNote(note, newPassword);
|
||||
@@ -106,7 +106,7 @@ export default class Vault {
|
||||
...this._db.notes.locked.map((note) => note.id)
|
||||
);
|
||||
}
|
||||
await this._context.remove("vaultKey");
|
||||
await this._storage.remove("vaultKey");
|
||||
this._password = null;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export default class Vault {
|
||||
}
|
||||
|
||||
async exists(vaultKey) {
|
||||
if (!vaultKey) vaultKey = await this._context.read("vaultKey");
|
||||
if (!vaultKey) vaultKey = await this._storage.read("vaultKey");
|
||||
return vaultKey && vaultKey.cipher && vaultKey.iv;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export default class Vault {
|
||||
|
||||
/** @private */
|
||||
async _encryptContent(contentId, content, type, password) {
|
||||
let encryptedContent = await this._context.encrypt(
|
||||
let encryptedContent = await this._storage.encrypt(
|
||||
{ password },
|
||||
JSON.stringify(content)
|
||||
);
|
||||
@@ -191,7 +191,7 @@ export default class Vault {
|
||||
async _decryptContent(contentId, password) {
|
||||
let encryptedContent = await this._db.content.raw(contentId);
|
||||
|
||||
let decryptedContent = await this._context.decrypt(
|
||||
let decryptedContent = await this._storage.decrypt(
|
||||
{ password },
|
||||
encryptedContent.data
|
||||
);
|
||||
@@ -257,12 +257,12 @@ export default class Vault {
|
||||
|
||||
/** @inner */
|
||||
async _getKey() {
|
||||
if (await this.exists()) return await this._context.read("vaultKey");
|
||||
if (await this.exists()) return await this._storage.read("vaultKey");
|
||||
}
|
||||
|
||||
/** @inner */
|
||||
async _setKey(vaultKey) {
|
||||
if (!vaultKey) return;
|
||||
await this._context.write("vaultKey", vaultKey);
|
||||
await this._storage.write("vaultKey", vaultKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Collection from "./collection";
|
||||
import id from "../utils/id";
|
||||
import { deleteItem } from "../utils/array";
|
||||
import { deleteItem, hasItem } from "../utils/array";
|
||||
import hosts from "../utils/constants";
|
||||
import { EV, EVENTS } from "../common";
|
||||
import dataurl from "../utils/dataurl";
|
||||
|
||||
export default class Attachments extends Collection {
|
||||
constructor(db, name, cached) {
|
||||
@@ -9,12 +11,13 @@ export default class Attachments extends Collection {
|
||||
this.key = null;
|
||||
}
|
||||
|
||||
async _initEncryptionKey() {
|
||||
async _getEncryptionKey() {
|
||||
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."
|
||||
);
|
||||
return this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,13 +81,9 @@ export default class Attachments extends Collection {
|
||||
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
||||
if (!attachment || !deleteItem(attachment.noteIds, noteId)) return;
|
||||
if (!attachment.noteIds.length) {
|
||||
const isDeleted = await this._db.fs.deleteFile(attachment.metadata.hash);
|
||||
if (!isDeleted) return;
|
||||
attachment.dateDeleted = Date.now();
|
||||
const token = await this._db.user.tokenManager.getToken();
|
||||
const result = await this.fs.deleteFile(attachment.metadata.hash, {
|
||||
url: `${hosts.API_HOST}/s3?name=${attachment.metadata.hash}`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!result) return;
|
||||
}
|
||||
return this._collection.updateItem(attachment);
|
||||
}
|
||||
@@ -92,10 +91,10 @@ export default class Attachments extends Collection {
|
||||
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.metadata.hash,
|
||||
this.key,
|
||||
await this._getEncryptionKey(),
|
||||
{
|
||||
iv: attachment.iv,
|
||||
salt: attachment.salt,
|
||||
@@ -104,8 +103,7 @@ export default class Attachments extends Collection {
|
||||
outputType: "base64",
|
||||
}
|
||||
);
|
||||
attachment.data = data;
|
||||
return attachment;
|
||||
return { data, ...attachment };
|
||||
}
|
||||
|
||||
async attachment(id) {
|
||||
@@ -121,11 +119,49 @@ export default class Attachments extends Collection {
|
||||
}
|
||||
|
||||
async save(data, type) {
|
||||
await this._initEncryptionKey();
|
||||
return await this._db.fs.writeEncrypted(null, {
|
||||
return await this._db.fs.writeEncrypted(
|
||||
null,
|
||||
data,
|
||||
type,
|
||||
key: this.key,
|
||||
await this._getEncryptionKey()
|
||||
);
|
||||
}
|
||||
|
||||
async download(noteId) {
|
||||
const attachments = this.media.filter((attachment) =>
|
||||
hasItem(attachment.noteIds, noteId)
|
||||
);
|
||||
console.log("Downloading attachments", attachments);
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
EV.publish(EVENTS.attachmentsLoading, {
|
||||
type: "download",
|
||||
total: attachments.length,
|
||||
current: i,
|
||||
});
|
||||
|
||||
const { hash } = attachments[i].metadata;
|
||||
|
||||
const attachmentExists = await this._db.fs.exists(hash);
|
||||
if (attachmentExists) continue;
|
||||
|
||||
const isDownloaded = await this._db.fs.downloadFile(hash);
|
||||
if (!isDownloaded) continue;
|
||||
|
||||
const attachment = await this.get(hash);
|
||||
if (!attachment) continue;
|
||||
|
||||
EV.publish(EVENTS.mediaAttachmentDownloaded, {
|
||||
hash,
|
||||
src: dataurl.fromObject({
|
||||
type: attachment.metadata.type,
|
||||
data: attachment.data,
|
||||
}),
|
||||
});
|
||||
}
|
||||
EV.publish(EVENTS.attachmentsLoading, {
|
||||
type: "download",
|
||||
total: attachments.length,
|
||||
current: attachments.length,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ class Collection {
|
||||
*/
|
||||
constructor(db, name, cached) {
|
||||
this._db = db;
|
||||
if (cached) this._collection = new CachedCollection(this._db.context, name);
|
||||
else this._collection = new IndexedCollection(this._db.context, name);
|
||||
if (cached) this._collection = new CachedCollection(this._db.storage, name);
|
||||
else this._collection = new IndexedCollection(this._db.storage, name);
|
||||
}
|
||||
}
|
||||
export default Collection;
|
||||
|
||||
@@ -37,6 +37,8 @@ export const EVENTS = {
|
||||
appRefreshRequested: "app:refreshRequested",
|
||||
noteRemoved: "note:removed",
|
||||
tokenRefreshed: "token:refreshed",
|
||||
attachmentsLoading: "attachments:loading",
|
||||
mediaAttachmentDownloaded: "attachments:mediaDownloaded",
|
||||
};
|
||||
|
||||
export const CURRENT_DATABASE_VERSION = 5.2;
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class Backup {
|
||||
}
|
||||
|
||||
lastBackupTime() {
|
||||
return this._db.context.read("lastBackupTime");
|
||||
return this._db.storage.read("lastBackupTime");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,22 +35,22 @@ export default class Backup {
|
||||
if (!validTypes.some((t) => t === type))
|
||||
throw new Error("Invalid type. It must be one of 'mobile' or 'web'.");
|
||||
|
||||
let keys = await this._db.context.getAllKeys();
|
||||
let keys = await this._db.storage.getAllKeys();
|
||||
let data = filterData(
|
||||
Object.fromEntries(await this._db.context.readMulti(keys))
|
||||
Object.fromEntries(await this._db.storage.readMulti(keys))
|
||||
);
|
||||
|
||||
let hash = {};
|
||||
|
||||
if (encrypt) {
|
||||
const key = await this._db.user.getEncryptionKey();
|
||||
data = await this._db.context.encrypt(key, JSON.stringify(data));
|
||||
data = await this._db.storage.encrypt(key, JSON.stringify(data));
|
||||
} else {
|
||||
hash = { hash: SparkMD5.hash(JSON.stringify(data)), hash_type: "md5" };
|
||||
}
|
||||
|
||||
// save backup time
|
||||
await this._db.context.write("lastBackupTime", Date.now());
|
||||
await this._db.storage.write("lastBackupTime", Date.now());
|
||||
return JSON.stringify({
|
||||
version: CURRENT_DATABASE_VERSION,
|
||||
type,
|
||||
@@ -77,7 +77,7 @@ export default class Backup {
|
||||
//check if we have encrypted data
|
||||
if (db.salt && db.iv) {
|
||||
if (!key) key = await this._db.user.getEncryptionKey();
|
||||
backup.data = JSON.parse(await this._db.context.decrypt(key, db));
|
||||
backup.data = JSON.parse(await this._db.storage.decrypt(key, db));
|
||||
} else if (!this._verify(backup))
|
||||
throw new Error("Backup file has been tempered, aborting...");
|
||||
|
||||
|
||||
65
packages/core/database/fs.js
Normal file
65
packages/core/database/fs.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import hosts from "../utils/constants";
|
||||
import TokenManager from "../api/token-manager";
|
||||
|
||||
export default class FileStorage {
|
||||
constructor(fs, storage) {
|
||||
this.fs = fs;
|
||||
this.tokenManager = new TokenManager(storage);
|
||||
}
|
||||
|
||||
async downloadFile(hash) {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
return await this.fs.downloadFile(hash, {
|
||||
url,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile(hash) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const url = await this._getPresignedURL(hash, token, "PUT");
|
||||
return await this.fs.uploadFile(hash, { url });
|
||||
}
|
||||
|
||||
readEncrypted(filename, encryptionKey, cipherData) {
|
||||
return this.fs.readEncrypted(filename, encryptionKey, cipherData);
|
||||
}
|
||||
|
||||
writeEncrypted(filename, data, type, encryptionKey) {
|
||||
return this._db.fs.writeEncrypted(filename, {
|
||||
data,
|
||||
type,
|
||||
key: encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteFile(filename) {
|
||||
const token = await this.tokenManager.getToken();
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
return await this.fs.deleteFile(filename, {
|
||||
url,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filename
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
exists(filename) {
|
||||
return this.fs.exists(filename);
|
||||
}
|
||||
|
||||
async _getPresignedURL(filename, token, verb) {
|
||||
const response = await fetch(`${hosts.API_HOST}/s3?name=${filename}`, {
|
||||
method: verb,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (response.ok) return await response.text();
|
||||
throw new Error("Couldn't get presigned url.");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ function toObject(dataurl) {
|
||||
}
|
||||
|
||||
function fromObject({ type, data }) {
|
||||
//const { groups } = REGEX.exec(dataurl);
|
||||
if (REGEX.test(data)) return data;
|
||||
return `data:${type};base64,${data}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ class EventManager {
|
||||
subscribe(name, handler, once = false) {
|
||||
if (!name || !handler) throw new Error("name and handler are required.");
|
||||
this._registry.set(handler, { name, once });
|
||||
return { unsubscribe: () => this.unsubscribe(name, handler) };
|
||||
}
|
||||
|
||||
unsubscribe(_name, handler) {
|
||||
this._registry.delete(handler);
|
||||
return this._registry.delete(handler);
|
||||
}
|
||||
|
||||
publish(name, ...args) {
|
||||
|
||||
Reference in New Issue
Block a user