mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
WIP blob
This commit is contained in:
parent
1faf8225c7
commit
5a8e216dec
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "blobs" (
|
||||
`blobId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`blobId`)
|
||||
);
|
||||
|
||||
ALTER TABLE notes ADD blobId TEXT DEFAULT NULL;
|
||||
ALTER TABLE note_revisions ADD blobId TEXT DEFAULT NULL;
|
63
db/migrations/0215__move_content_into_blobs.js
Normal file
63
db/migrations/0215__move_content_into_blobs.js
Normal file
@ -0,0 +1,63 @@
|
||||
const sql = require("../../src/services/sql.js");
|
||||
module.exports = () => {
|
||||
const sql = require("../../src/services/sql");
|
||||
const utils = require("../../src/services/utils");
|
||||
|
||||
const existingBlobIds = new Set();
|
||||
|
||||
for (const noteId of sql.getColumn(`SELECT noteId FROM note_contents`)) {
|
||||
const row = sql.getRow(`SELECT noteId, content, dateModified, utcDateModified FROM note_contents WHERE noteId = ?`, [noteId]);
|
||||
const blobId = utils.hashedBlobId(row.content);
|
||||
|
||||
if (!existingBlobIds.has(blobId)) {
|
||||
existingBlobIds.add(blobId);
|
||||
|
||||
sql.insert('blobs', {
|
||||
blobId,
|
||||
content: row.content,
|
||||
dateModified: row.dateModified,
|
||||
utcDateModified: row.utcDateModified
|
||||
});
|
||||
|
||||
sql.execute("UPDATE entity_changes SET entityName = 'blobs', entityId = ? WHERE entityName = 'note_contents' AND entityId = ?", [blobId, row.noteId]);
|
||||
} else {
|
||||
// duplicates
|
||||
sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_contents' AND entityId = ?", [row.noteId]);
|
||||
}
|
||||
|
||||
sql.execute('UPDATE notes SET blobId = ? WHERE noteId = ?', [blobId, row.noteId]);
|
||||
}
|
||||
|
||||
for (const noteRevisionId of sql.getColumn(`SELECT noteRevisionId FROM note_revision_contents`)) {
|
||||
const row = sql.getRow(`SELECT noteRevisionId, content, utcDateModified FROM note_revision_contents WHERE noteRevisionId = ?`, [noteRevisionId]);
|
||||
const blobId = utils.hashedBlobId(row.content);
|
||||
|
||||
if (!existingBlobIds.has(blobId)) {
|
||||
existingBlobIds.add(blobId);
|
||||
|
||||
sql.insert('blobs', {
|
||||
blobId,
|
||||
content: row.content,
|
||||
dateModified: row.utcDateModified,
|
||||
utcDateModified: row.utcDateModified
|
||||
});
|
||||
|
||||
sql.execute("UPDATE entity_changes SET entityName = 'blobs', entityId = ? WHERE entityName = 'note_revision_contents' AND entityId = ?", [blobId, row.noteRevisionId]);
|
||||
} else {
|
||||
// duplicates
|
||||
sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_revision_contents' AND entityId = ?", [row.noteId]);
|
||||
}
|
||||
|
||||
sql.execute('UPDATE note_revisions SET blobId = ? WHERE noteRevisionId = ?', [blobId, row.noteRevisionId]);
|
||||
}
|
||||
|
||||
const notesWithoutBlobIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId IS NULL");
|
||||
if (notesWithoutBlobIds.length > 0) {
|
||||
throw new Error("BlobIds were not filled correctly in notes: " + JSON.stringify(notesWithoutBlobIds));
|
||||
}
|
||||
|
||||
const noteRevisionsWithoutBlobIds = sql.getColumn("SELECT noteRevisionId FROM note_revisions WHERE blobId IS NULL");
|
||||
if (noteRevisionsWithoutBlobIds.length > 0) {
|
||||
throw new Error("BlobIds were not filled correctly in note revisions: " + JSON.stringify(noteRevisionsWithoutBlobIds));
|
||||
}
|
||||
};
|
2
db/migrations/0216__drop_content_tables.sql
Normal file
2
db/migrations/0216__drop_content_tables.sql
Normal file
@ -0,0 +1,2 @@
|
||||
DROP TABLE note_contents;
|
||||
DROP TABLE note_revision_contents;
|
@ -6,14 +6,11 @@ CREATE TABLE IF NOT EXISTS "note_ancillaries"
|
||||
mime TEXT not null,
|
||||
isProtected INT not null DEFAULT 0,
|
||||
contentCheckSum TEXT not null,
|
||||
blobId TEXT not null,
|
||||
utcDateModified TEXT not null,
|
||||
isDeleted INT not null,
|
||||
`deleteId` TEXT DEFAULT NULL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "note_ancillary_contents" (`noteAncillaryId` TEXT NOT NULL PRIMARY KEY,
|
||||
`content` TEXT DEFAULT NULL,
|
||||
`utcDateModified` TEXT NOT NULL);
|
||||
|
||||
CREATE INDEX IDX_note_ancillaries_name
|
||||
on note_ancillaries (name);
|
||||
CREATE UNIQUE INDEX IDX_note_ancillaries_noteId_name
|
@ -30,7 +30,7 @@ function load() {
|
||||
// using raw query and passing arrays to avoid allocating new objects
|
||||
// this is worth it for becca load since it happens every run and blocks the app until finished
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`)) {
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`)) {
|
||||
new BNote().update(row).init();
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
row.type,
|
||||
row.mime,
|
||||
row.isProtected,
|
||||
row.blobId,
|
||||
row.dateCreated,
|
||||
row.dateModified,
|
||||
row.utcDateCreated,
|
||||
@ -53,19 +54,21 @@ class BNote extends AbstractBeccaEntity {
|
||||
]);
|
||||
}
|
||||
|
||||
update([noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
|
||||
update([noteId, title, type, mime, isProtected, blobId, dateCreated, dateModified, utcDateCreated, utcDateModified]) {
|
||||
// ------ Database persisted attributes ------
|
||||
|
||||
/** @type {string} */
|
||||
this.noteId = noteId;
|
||||
/** @type {string} */
|
||||
this.title = title;
|
||||
/** @type {boolean} */
|
||||
this.isProtected = !!isProtected;
|
||||
/** @type {string} */
|
||||
this.type = type;
|
||||
/** @type {string} */
|
||||
this.mime = mime;
|
||||
/** @type {boolean} */
|
||||
this.isProtected = !!isProtected;
|
||||
/** @type {string} */
|
||||
this.blobId = blobId;
|
||||
/** @type {string} */
|
||||
this.dateCreated = dateCreated || dateUtils.localNowDateTime();
|
||||
/** @type {string} */
|
||||
@ -206,14 +209,14 @@ class BNote extends AbstractBeccaEntity {
|
||||
|
||||
/** @returns {*} */
|
||||
getContent(silentNotFoundError = false) {
|
||||
const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
|
||||
const row = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
||||
|
||||
if (!row) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot find note content for noteId=${this.noteId}`);
|
||||
throw new Error(`Cannot find note content for noteId '${this.noteId}', blobId '${this.blobId}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,8 +248,8 @@ class BNote extends AbstractBeccaEntity {
|
||||
LENGTH(content) AS contentLength,
|
||||
dateModified,
|
||||
utcDateModified
|
||||
FROM note_contents
|
||||
WHERE noteId = ?`, [this.noteId]);
|
||||
FROM blobs
|
||||
WHERE blobId = ?`, [this.blobId]);
|
||||
}
|
||||
|
||||
get dateCreatedObj() {
|
||||
@ -276,6 +279,10 @@ class BNote extends AbstractBeccaEntity {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
isHot() {
|
||||
return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type);
|
||||
}
|
||||
|
||||
setContent(content, ignoreMissingProtectedSession = false) {
|
||||
if (content === null || content === undefined) {
|
||||
throw new Error(`Cannot set null content to note '${this.noteId}'`);
|
||||
@ -288,29 +295,41 @@ class BNote extends AbstractBeccaEntity {
|
||||
content = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
||||
}
|
||||
|
||||
const pojo = {
|
||||
noteId: this.noteId,
|
||||
content: content,
|
||||
dateModified: dateUtils.localNowDateTime(),
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
pojo.content = protectedSessionService.encrypt(pojo.content);
|
||||
content = protectedSessionService.encrypt(content);
|
||||
}
|
||||
else if (!ignoreMissingProtectedSession) {
|
||||
throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`);
|
||||
}
|
||||
}
|
||||
|
||||
sql.upsert("note_contents", "noteId", pojo);
|
||||
let newBlobId;
|
||||
let blobNeedsInsert;
|
||||
|
||||
const hash = utils.hash(`${this.noteId}|${pojo.content.toString()}`);
|
||||
if (this.isHot()) {
|
||||
newBlobId = this.blobId || utils.randomBlobId();
|
||||
blobNeedsInsert = true;
|
||||
} else {
|
||||
newBlobId = utils.hashedBlobId(content);
|
||||
blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
|
||||
}
|
||||
|
||||
if (blobNeedsInsert) {
|
||||
const pojo = {
|
||||
blobId: this.blobId,
|
||||
content: content,
|
||||
dateModified: dateUtils.localNowDateTime(),
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
sql.upsert("blobs", "blobId", pojo);
|
||||
|
||||
const hash = utils.hash(`${this.blobId}|${pojo.content.toString()}`);
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_contents',
|
||||
entityId: this.noteId,
|
||||
entityName: 'blobs',
|
||||
entityId: this.blobId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: pojo.utcDateModified,
|
||||
@ -318,11 +337,17 @@ class BNote extends AbstractBeccaEntity {
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CHANGED, {
|
||||
entityName: 'note_contents',
|
||||
entityName: 'blobs',
|
||||
entity: this
|
||||
});
|
||||
}
|
||||
|
||||
if (newBlobId !== this.blobId) {
|
||||
this.blobId = newBlobId;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
setJsonContent(content) {
|
||||
this.setContent(JSON.stringify(content, null, '\t'));
|
||||
}
|
||||
@ -1517,6 +1542,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
isProtected: this.isProtected,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
blobId: this.blobId,
|
||||
isDeleted: false,
|
||||
dateCreated: this.dateCreated,
|
||||
dateModified: this.dateModified,
|
||||
|
@ -35,6 +35,8 @@ class BNoteRevision extends AbstractBeccaEntity {
|
||||
/** @type {string} */
|
||||
this.title = row.title;
|
||||
/** @type {string} */
|
||||
this.blobId = row.blobId;
|
||||
/** @type {string} */
|
||||
this.dateLastEdited = row.dateLastEdited;
|
||||
/** @type {string} */
|
||||
this.dateCreated = row.dateCreated;
|
||||
@ -74,14 +76,14 @@ class BNoteRevision extends AbstractBeccaEntity {
|
||||
|
||||
/** @returns {*} */
|
||||
getContent(silentNotFoundError = false) {
|
||||
const res = sql.getRow(`SELECT content FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
|
||||
const res = sql.getRow(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
||||
|
||||
if (!res) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot find note revision content for noteRevisionId=${this.noteRevisionId}`);
|
||||
throw new Error(`Cannot find note revision content for noteRevisionId '${this.noteRevisionId}', blobId '${this.blobId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,28 +109,34 @@ class BNoteRevision extends AbstractBeccaEntity {
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
content = protectedSessionService.encrypt(content);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot update content of noteRevisionId '${this.noteRevisionId}' since we're out of protected session.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.blobId = utils.hashedBlobId(content);
|
||||
|
||||
const blobAlreadyExists = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [this.blobId]);
|
||||
|
||||
if (!blobAlreadyExists) {
|
||||
const pojo = {
|
||||
noteRevisionId: this.noteRevisionId,
|
||||
blobId: this.blobId,
|
||||
content: content,
|
||||
dateModified: dateUtils.localNowDate(),
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
pojo.content = protectedSessionService.encrypt(pojo.content);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot update content of noteRevisionId=${this.noteRevisionId} since we're out of protected session.`);
|
||||
}
|
||||
}
|
||||
|
||||
sql.upsert("note_revision_contents", "noteRevisionId", pojo);
|
||||
sql.insert("blobs", pojo);
|
||||
|
||||
const hash = utils.hash(`${this.noteRevisionId}|${pojo.content.toString()}`);
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_revision_contents',
|
||||
entityId: this.noteRevisionId,
|
||||
entityName: 'blobs',
|
||||
entityId: this.blobId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: this.getUtcDateChanged(),
|
||||
@ -136,15 +144,7 @@ class BNoteRevision extends AbstractBeccaEntity {
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {{contentLength, dateModified, utcDateModified}} */
|
||||
getContentMetadata() {
|
||||
return sql.getRow(`
|
||||
SELECT
|
||||
LENGTH(content) AS contentLength,
|
||||
dateModified,
|
||||
utcDateModified
|
||||
FROM note_revision_contents
|
||||
WHERE noteRevisionId = ?`, [this.noteRevisionId]);
|
||||
this.save(); // saving this.blobId
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
@ -161,6 +161,7 @@ class BNoteRevision extends AbstractBeccaEntity {
|
||||
mime: this.mime,
|
||||
isProtected: this.isProtected,
|
||||
title: this.title,
|
||||
blobId: this.blobId,
|
||||
dateLastEdited: this.dateLastEdited,
|
||||
dateCreated: this.dateCreated,
|
||||
utcDateLastEdited: this.utcDateLastEdited,
|
||||
|
@ -27,7 +27,7 @@ class FNoteComplement {
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
|
||||
// "combined" date modified give larger out of note's and note_content's dateModified
|
||||
// "combined" date modified give larger out of note's and blob's dateModified
|
||||
|
||||
/** @type {string} */
|
||||
this.combinedDateModified = row.combinedDateModified;
|
||||
|
@ -35,7 +35,7 @@ class Froca {
|
||||
this.attributes = {};
|
||||
|
||||
/** @type {Object.<string, Promise<FNoteComplement>>} */
|
||||
this.noteComplementPromises = {};
|
||||
this.blobPromises = {};
|
||||
|
||||
this.addResp(resp);
|
||||
}
|
||||
@ -314,20 +314,20 @@ class Froca {
|
||||
* @returns {Promise<FNoteComplement>}
|
||||
*/
|
||||
async getNoteComplement(noteId) {
|
||||
if (!this.noteComplementPromises[noteId]) {
|
||||
this.noteComplementPromises[noteId] = server.get(`notes/${noteId}`)
|
||||
if (!this.blobPromises[noteId]) {
|
||||
this.blobPromises[noteId] = server.get(`notes/${noteId}`)
|
||||
.then(row => new FNoteComplement(row))
|
||||
.catch(e => console.error(`Cannot get note complement for note '${noteId}'`));
|
||||
|
||||
// we don't want to keep large payloads forever in memory, so we clean that up quite quickly
|
||||
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
|
||||
// this is also a workaround for missing invalidation after change
|
||||
this.noteComplementPromises[noteId].then(
|
||||
() => setTimeout(() => this.noteComplementPromises[noteId] = null, 1000)
|
||||
this.blobPromises[noteId].then(
|
||||
() => setTimeout(() => this.blobPromises[noteId] = null, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
return await this.noteComplementPromises[noteId];
|
||||
return await this.blobPromises[noteId];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,10 @@ async function processEntityChanges(entityChanges) {
|
||||
processAttributeChange(loadResults, ec);
|
||||
} else if (ec.entityName === 'note_reordering') {
|
||||
processNoteReordering(loadResults, ec);
|
||||
} else if (ec.entityName === 'note_contents') {
|
||||
delete froca.noteComplementPromises[ec.entityId];
|
||||
} else if (ec.entityName === 'blobs') {
|
||||
delete froca.blobPromises[ec.entityId];
|
||||
|
||||
loadResults.addNoteContent(ec.entityId, ec.componentId);
|
||||
loadResults.addNoteContent(ec.noteIds, ec.componentId);
|
||||
} else if (ec.entityName === 'note_revisions') {
|
||||
loadResults.addNoteRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === 'note_revision_contents') {
|
||||
|
@ -94,9 +94,11 @@ export default class LoadResults {
|
||||
return componentIds && componentIds.find(sId => sId !== componentId) !== undefined;
|
||||
}
|
||||
|
||||
addNoteContent(noteId, componentId) {
|
||||
addNoteContent(noteIds, componentId) {
|
||||
for (const noteId of noteIds) {
|
||||
this.contentNoteIdToComponentId.push({noteId, componentId});
|
||||
}
|
||||
}
|
||||
|
||||
isNoteContentReloaded(noteId, componentId) {
|
||||
if (!noteId) {
|
||||
|
@ -4,7 +4,7 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 215;
|
||||
const APP_DB_VERSION = 217;
|
||||
const SYNC_VERSION = 30;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
@ -383,86 +383,86 @@ class ConsistencyChecks {
|
||||
}
|
||||
});
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT notes.noteId, notes.isProtected, notes.type, notes.mime
|
||||
FROM notes
|
||||
LEFT JOIN note_contents USING (noteId)
|
||||
WHERE note_contents.noteId IS NULL`,
|
||||
({noteId, isProtected, type, mime}) => {
|
||||
if (this.autoFix) {
|
||||
// it might be possible that the note_content is not available only because of the interrupted
|
||||
// sync, and it will come later. It's therefore important to guarantee that this artifical
|
||||
// record won't overwrite the real one coming from the sync.
|
||||
const fakeDate = "2000-01-01 00:00:00Z";
|
||||
|
||||
// manually creating row since this can also affect deleted notes
|
||||
sql.upsert("note_contents", "noteId", {
|
||||
noteId: noteId,
|
||||
content: getBlankContent(isProtected, type, mime),
|
||||
utcDateModified: fakeDate,
|
||||
dateModified: fakeDate
|
||||
});
|
||||
|
||||
const hash = utils.hash(utils.randomString(10));
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_contents',
|
||||
entityId: noteId,
|
||||
hash: hash,
|
||||
isErased: false,
|
||||
utcDateChanged: fakeDate,
|
||||
isSynced: true
|
||||
});
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Note '${noteId}' content was set to empty string since there was no corresponding row`);
|
||||
} else {
|
||||
logError(`Note '${noteId}' content row does not exist`);
|
||||
}
|
||||
});
|
||||
// this.findAndFixIssues(`
|
||||
// SELECT notes.noteId, notes.isProtected, notes.type, notes.mime
|
||||
// FROM notes
|
||||
// LEFT JOIN note_contents USING (noteId)
|
||||
// WHERE note_contents.noteId IS NULL`,
|
||||
// ({noteId, isProtected, type, mime}) => {
|
||||
// if (this.autoFix) {
|
||||
// // it might be possible that the note_content is not available only because of the interrupted
|
||||
// // sync, and it will come later. It's therefore important to guarantee that this artifical
|
||||
// // record won't overwrite the real one coming from the sync.
|
||||
// const fakeDate = "2000-01-01 00:00:00Z";
|
||||
//
|
||||
// // manually creating row since this can also affect deleted notes
|
||||
// sql.upsert("note_contents", "noteId", {
|
||||
// noteId: noteId,
|
||||
// content: getBlankContent(isProtected, type, mime),
|
||||
// utcDateModified: fakeDate,
|
||||
// dateModified: fakeDate
|
||||
// });
|
||||
//
|
||||
// const hash = utils.hash(utils.randomString(10));
|
||||
//
|
||||
// entityChangesService.addEntityChange({
|
||||
// entityName: 'note_contents',
|
||||
// entityId: noteId,
|
||||
// hash: hash,
|
||||
// isErased: false,
|
||||
// utcDateChanged: fakeDate,
|
||||
// isSynced: true
|
||||
// });
|
||||
//
|
||||
// this.reloadNeeded = true;
|
||||
//
|
||||
// logFix(`Note '${noteId}' content was set to empty string since there was no corresponding row`);
|
||||
// } else {
|
||||
// logError(`Note '${noteId}' content row does not exist`);
|
||||
// }
|
||||
// });
|
||||
|
||||
if (sqlInit.getDbSize() < 500000) {
|
||||
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT notes.noteId, notes.type, notes.mime
|
||||
FROM notes
|
||||
JOIN note_contents USING (noteId)
|
||||
WHERE isDeleted = 0
|
||||
AND isProtected = 0
|
||||
AND content IS NULL`,
|
||||
({noteId, type, mime}) => {
|
||||
if (this.autoFix) {
|
||||
const note = becca.getNote(noteId);
|
||||
const blankContent = getBlankContent(false, type, mime);
|
||||
note.setContent(blankContent);
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Note '${noteId}' content was set to '${blankContent}' since it was null even though it is not deleted`);
|
||||
} else {
|
||||
logError(`Note '${noteId}' content is null even though it is not deleted`);
|
||||
}
|
||||
});
|
||||
// this.findAndFixIssues(`
|
||||
// SELECT notes.noteId, notes.type, notes.mime
|
||||
// FROM notes
|
||||
// JOIN note_contents USING (noteId)
|
||||
// WHERE isDeleted = 0
|
||||
// AND isProtected = 0
|
||||
// AND content IS NULL`,
|
||||
// ({noteId, type, mime}) => {
|
||||
// if (this.autoFix) {
|
||||
// const note = becca.getNote(noteId);
|
||||
// const blankContent = getBlankContent(false, type, mime);
|
||||
// note.setContent(blankContent);
|
||||
//
|
||||
// this.reloadNeeded = true;
|
||||
//
|
||||
// logFix(`Note '${noteId}' content was set to '${blankContent}' since it was null even though it is not deleted`);
|
||||
// } else {
|
||||
// logError(`Note '${noteId}' content is null even though it is not deleted`);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT note_revisions.noteRevisionId
|
||||
FROM note_revisions
|
||||
LEFT JOIN note_revision_contents USING (noteRevisionId)
|
||||
WHERE note_revision_contents.noteRevisionId IS NULL`,
|
||||
({noteRevisionId}) => {
|
||||
if (this.autoFix) {
|
||||
noteRevisionService.eraseNoteRevisions([noteRevisionId]);
|
||||
|
||||
this.reloadNeeded = true;
|
||||
|
||||
logFix(`Note revision content '${noteRevisionId}' was set to erased since it did not exist.`);
|
||||
} else {
|
||||
logError(`Note revision content '${noteRevisionId}' does not exist`);
|
||||
}
|
||||
});
|
||||
// this.findAndFixIssues(`
|
||||
// SELECT note_revisions.noteRevisionId
|
||||
// FROM note_revisions
|
||||
// LEFT JOIN note_revision_contents USING (noteRevisionId)
|
||||
// WHERE note_revision_contents.noteRevisionId IS NULL`,
|
||||
// ({noteRevisionId}) => {
|
||||
// if (this.autoFix) {
|
||||
// noteRevisionService.eraseNoteRevisions([noteRevisionId]);
|
||||
//
|
||||
// this.reloadNeeded = true;
|
||||
//
|
||||
// logFix(`Note revision content '${noteRevisionId}' was set to erased since it did not exist.`);
|
||||
// } else {
|
||||
// logError(`Note revision content '${noteRevisionId}' does not exist`);
|
||||
// }
|
||||
// });
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT parentNoteId
|
||||
@ -656,11 +656,11 @@ class ConsistencyChecks {
|
||||
|
||||
findEntityChangeIssues() {
|
||||
this.runEntityChangeChecks("notes", "noteId");
|
||||
this.runEntityChangeChecks("note_contents", "noteId");
|
||||
//this.runEntityChangeChecks("note_contents", "noteId");
|
||||
this.runEntityChangeChecks("note_revisions", "noteRevisionId");
|
||||
this.runEntityChangeChecks("note_revision_contents", "noteRevisionId");
|
||||
//this.runEntityChangeChecks("note_revision_contents", "noteRevisionId");
|
||||
this.runEntityChangeChecks("note_ancillaries", "noteAncillaryId");
|
||||
this.runEntityChangeChecks("note_ancillary_contents", "noteAncillaryId");
|
||||
//this.runEntityChangeChecks("note_ancillary_contents", "noteAncillaryId");
|
||||
this.runEntityChangeChecks("branches", "branchId");
|
||||
this.runEntityChangeChecks("attributes", "attributeId");
|
||||
this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
|
||||
|
@ -25,6 +25,19 @@ function md5(content) {
|
||||
return crypto.createHash('md5').update(content).digest('hex');
|
||||
}
|
||||
|
||||
function hashedBlobId(content) {
|
||||
// sha512 is faster than sha256
|
||||
const base64Hash = crypto.createHash('sha512').update(content).digest('base64');
|
||||
|
||||
// 20 characters of base64 gives us 120 bit of entropy which is plenty enough
|
||||
return base64Hash.substr(0, 20);
|
||||
}
|
||||
|
||||
function randomBlobId(content) {
|
||||
// underscore prefix to easily differentiate the random as opposed to hashed
|
||||
return '_' + randomString(19);
|
||||
}
|
||||
|
||||
function toBase64(plainText) {
|
||||
return Buffer.from(plainText).toString('base64');
|
||||
}
|
||||
@ -343,5 +356,7 @@ module.exports = {
|
||||
deferred,
|
||||
removeDiacritic,
|
||||
normalize,
|
||||
filterAttributeName
|
||||
filterAttributeName,
|
||||
hashedBlobId,
|
||||
randomBlobId
|
||||
};
|
||||
|
@ -129,13 +129,14 @@ function fillInAdditionalProperties(entityChange) {
|
||||
entityChange.positions[childBranch.branchId] = childBranch.notePosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (entityChange.entityName === 'options') {
|
||||
} else if (entityChange.entityName === 'options') {
|
||||
entityChange.entity = becca.getOption(entityChange.entityId);
|
||||
|
||||
if (!entityChange.entity) {
|
||||
entityChange.entity = sql.getRow(`SELECT * FROM options WHERE name = ?`, [entityChange.entityId]);
|
||||
}
|
||||
} else if (entityChange.entityName === 'blob') {
|
||||
entityChange.noteIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId = ? AND isDeleted = 0", [entityChange.entityId]);
|
||||
}
|
||||
|
||||
if (entityChange.entity instanceof AbstractBeccaEntity) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user