mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
note ancillary/attachment backported from dev branch
This commit is contained in:
parent
6b4800d2d6
commit
929f8ef720
@ -4,6 +4,7 @@ UPDATE notes SET title = 'title' WHERE noteId != 'root' AND noteId NOT LIKE '\_%
|
||||
UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL;
|
||||
UPDATE note_revisions SET title = 'title';
|
||||
UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL;
|
||||
UPDATE note_ancillary_contents SET content = 'text' WHERE content IS NOT NULL;
|
||||
|
||||
UPDATE attributes SET name = 'name', value = 'value'
|
||||
WHERE type = 'label'
|
||||
|
20
db/migrations/0213__note_ancillaries.sql
Normal file
20
db/migrations/0213__note_ancillaries.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS "note_ancillaries"
|
||||
(
|
||||
noteAncillaryId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
name TEXT not null,
|
||||
mime TEXT not null,
|
||||
isProtected INT not null DEFAULT 0,
|
||||
contentCheckSum 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
|
||||
on note_ancillaries (noteId, name);
|
@ -112,3 +112,21 @@ CREATE TABLE IF NOT EXISTS "recent_notes"
|
||||
notePath TEXT not null,
|
||||
utcDateCreated TEXT not null
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "note_ancillaries"
|
||||
(
|
||||
noteAncillaryId TEXT not null primary key,
|
||||
noteId TEXT not null,
|
||||
name TEXT not null,
|
||||
mime TEXT not null,
|
||||
isProtected INT not null DEFAULT 0,
|
||||
contentCheckSum 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
|
||||
on note_ancillaries (noteId, name);
|
||||
|
678
package-lock.json
generated
678
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,7 @@
|
||||
"@excalidraw/excalidraw": "0.14.2",
|
||||
"archiver": "5.3.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"axios": "1.3.3",
|
||||
"axios": "1.3.4",
|
||||
"better-sqlite3": "7.4.5",
|
||||
"chokidar": "3.5.3",
|
||||
"cls-hooked": "4.2.2",
|
||||
@ -64,7 +64,7 @@
|
||||
"ini": "3.0.1",
|
||||
"is-animated": "2.0.2",
|
||||
"is-svg": "4.3.2",
|
||||
"jimp": "0.22.4",
|
||||
"jimp": "0.22.7",
|
||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||
"jsdom": "21.1.0",
|
||||
"mime-types": "2.1.35",
|
||||
@ -101,7 +101,7 @@
|
||||
"electron-rebuild": "3.2.9",
|
||||
"esm": "3.2.25",
|
||||
"jasmine": "4.5.0",
|
||||
"jsdoc": "4.0.1",
|
||||
"jsdoc": "4.0.2",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
"rcedit": "3.0.1",
|
||||
"webpack": "5.75.0",
|
||||
|
@ -121,6 +121,14 @@ class Becca {
|
||||
return row ? new BNoteRevision(row) : null;
|
||||
}
|
||||
|
||||
/** @returns {BNoteAncillary|null} */
|
||||
getNoteAncillary(noteAncillaryId) {
|
||||
const row = sql.getRow("SELECT * FROM note_ancillaries WHERE noteAncillaryId = ?", [noteAncillaryId]);
|
||||
|
||||
const BNoteAncillary = require("./entities/bnote_ancillary"); // avoiding circular dependency problems
|
||||
return row ? new BNoteAncillary(row) : null;
|
||||
}
|
||||
|
||||
/** @returns {BOption|null} */
|
||||
getOption(name) {
|
||||
return this.options[name];
|
||||
@ -143,6 +151,8 @@ class Becca {
|
||||
|
||||
if (entityName === 'note_revisions') {
|
||||
return this.getNoteRevision(entityId);
|
||||
} else if (entityName === 'note_ancillaries') {
|
||||
return this.getNoteAncillary(entityId);
|
||||
}
|
||||
|
||||
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g,
|
||||
|
@ -198,6 +198,10 @@ class BBranch extends AbstractBeccaEntity {
|
||||
relation.markAsDeleted(deleteId);
|
||||
}
|
||||
|
||||
for (const noteAncillary of note.getNoteAncillaries()) {
|
||||
noteAncillary.markAsDeleted(deleteId);
|
||||
}
|
||||
|
||||
note.markAsDeleted(deleteId);
|
||||
|
||||
return true;
|
||||
|
@ -8,6 +8,7 @@ const dateUtils = require('../../services/date_utils');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractBeccaEntity = require("./abstract_becca_entity");
|
||||
const BNoteRevision = require("./bnote_revision");
|
||||
const BNoteAncillary = require("./bnote_ancillary");
|
||||
const TaskContext = require("../../services/task_context");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
@ -1135,6 +1136,19 @@ class BNote extends AbstractBeccaEntity {
|
||||
.map(row => new BNoteRevision(row));
|
||||
}
|
||||
|
||||
/** @returns {BNoteAncillary[]} */
|
||||
getNoteAncillaries() {
|
||||
return sql.getRows("SELECT * FROM note_ancillaries WHERE noteId = ? AND isDeleted = 0", [this.noteId])
|
||||
.map(row => new BNoteAncillary(row));
|
||||
}
|
||||
|
||||
/** @returns {BNoteAncillary|undefined} */
|
||||
getNoteAncillaryByName(name) {
|
||||
return sql.getRows("SELECT * FROM note_ancillaries WHERE noteId = ? AND name = ? AND isDeleted = 0", [this.noteId, name])
|
||||
.map(row => new BNoteAncillary(row))
|
||||
[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
|
||||
*/
|
||||
@ -1462,6 +1476,31 @@ class BNote extends AbstractBeccaEntity {
|
||||
return noteRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {BNoteAncillary}
|
||||
*/
|
||||
saveNoteAncillary(name, mime, content) {
|
||||
let noteAncillary = this.getNoteAncillaryByName(name);
|
||||
|
||||
if (noteAncillary
|
||||
&& noteAncillary.mime === mime
|
||||
&& noteAncillary.contentCheckSum === noteAncillary.calculateCheckSum(content)) {
|
||||
|
||||
return noteAncillary; // no change
|
||||
}
|
||||
|
||||
noteAncillary = new BNoteAncillary({
|
||||
noteId: this.noteId,
|
||||
name,
|
||||
mime,
|
||||
isProtected: this.isProtected
|
||||
});
|
||||
|
||||
noteAncillary.setContent(content);
|
||||
|
||||
return noteAncillary;
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
161
src/becca/entities/bnote_ancillary.js
Normal file
161
src/becca/entities/bnote_ancillary.js
Normal file
@ -0,0 +1,161 @@
|
||||
"use strict";
|
||||
|
||||
const protectedSessionService = require('../../services/protected_session');
|
||||
const utils = require('../../services/utils');
|
||||
const sql = require('../../services/sql');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const becca = require('../becca');
|
||||
const entityChangesService = require('../../services/entity_changes');
|
||||
const AbstractBeccaEntity = require("./abstract_becca_entity");
|
||||
|
||||
/**
|
||||
* NoteAncillary represent data related/attached to the note. Conceptually similar to attributes, but intended for
|
||||
* larger amounts of data and generally not accessible to the user.
|
||||
*
|
||||
* @extends AbstractBeccaEntity
|
||||
*/
|
||||
class BNoteAncillary extends AbstractBeccaEntity {
|
||||
static get entityName() { return "note_ancillaries"; }
|
||||
static get primaryKeyName() { return "noteAncillaryId"; }
|
||||
static get hashedProperties() { return ["noteAncillaryId", "noteId", "name", "content", "utcDateModified"]; }
|
||||
|
||||
constructor(row) {
|
||||
super();
|
||||
|
||||
if (!row.noteId) {
|
||||
throw new Error("'noteId' must be given to initialize a NoteAncillary entity");
|
||||
}
|
||||
|
||||
if (!row.name) {
|
||||
throw new Error("'name' must be given to initialize a NoteAncillary entity");
|
||||
}
|
||||
|
||||
/** @type {string} needs to be set at the initialization time since it's used in the .setContent() */
|
||||
this.noteAncillaryId = row.noteAncillaryId || `${this.noteId}_${this.name}`;
|
||||
/** @type {string} */
|
||||
this.noteId = row.noteId;
|
||||
/** @type {string} */
|
||||
this.name = row.name;
|
||||
/** @type {string} */
|
||||
this.mime = row.mime;
|
||||
/** @type {boolean} */
|
||||
this.isProtected = !!row.isProtected;
|
||||
/** @type {string} */
|
||||
this.contentCheckSum = row.contentCheckSum;
|
||||
/** @type {string} */
|
||||
this.utcDateModified = row.utcDateModified;
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return becca.notes[this.noteId];
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if the note has string content (not binary) */
|
||||
isStringNote() {
|
||||
return utils.isStringNote(this.type, this.mime);
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
getContent(silentNotFoundError = false) {
|
||||
const res = sql.getRow(`SELECT content FROM note_ancillary_contents WHERE noteAncillaryId = ?`, [this.noteAncillaryId]);
|
||||
|
||||
if (!res) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Cannot find note ancillary content for noteAncillaryId=${this.noteAncillaryId}`);
|
||||
}
|
||||
}
|
||||
|
||||
let content = res.content;
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
content = protectedSessionService.decrypt(content);
|
||||
}
|
||||
else {
|
||||
content = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isStringNote()) {
|
||||
return content === null
|
||||
? ""
|
||||
: content.toString("UTF-8");
|
||||
}
|
||||
else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
sql.transactional(() => {
|
||||
this.contentCheckSum = this.calculateCheckSum(content);
|
||||
this.save(); // also explicitly save note_ancillary to update contentCheckSum
|
||||
|
||||
const pojo = {
|
||||
noteAncillaryId: this.noteAncillaryId,
|
||||
content: content,
|
||||
utcDateModified: dateUtils.utcNowDateTime()
|
||||
};
|
||||
|
||||
if (this.isProtected) {
|
||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||
pojo.content = protectedSessionService.encrypt(pojo.content);
|
||||
} else {
|
||||
throw new Error(`Cannot update content of noteAncillaryId=${this.noteAncillaryId} since we're out of protected session.`);
|
||||
}
|
||||
}
|
||||
|
||||
sql.upsert("note_ancillary_contents", "noteAncillaryId", pojo);
|
||||
|
||||
entityChangesService.addEntityChange({
|
||||
entityName: 'note_ancillary_contents',
|
||||
entityId: this.noteAncillaryId,
|
||||
hash: this.contentCheckSum,
|
||||
isErased: false,
|
||||
utcDateChanged: pojo.utcDateModified,
|
||||
isSynced: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
calculateCheckSum(content) {
|
||||
return utils.hash(`${this.noteAncillaryId}|${content.toString()}`);
|
||||
}
|
||||
|
||||
beforeSaving() {
|
||||
if (!this.name.match(/^[a-z0-9]+$/i)) {
|
||||
throw new Error(`Name must be alphanumerical, "${this.name}" given.`);
|
||||
}
|
||||
|
||||
this.noteAncillaryId = `${this.noteId}_${this.name}`;
|
||||
|
||||
super.beforeSaving();
|
||||
|
||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
noteAncillaryId: this.noteAncillaryId,
|
||||
noteId: this.noteId,
|
||||
name: this.name,
|
||||
mime: this.mime,
|
||||
isProtected: !!this.isProtected,
|
||||
contentCheckSum: this.contentCheckSum,
|
||||
isDeleted: false,
|
||||
utcDateModified: this.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
getPojoToSave() {
|
||||
const pojo = this.getPojo();
|
||||
delete pojo.content; // not getting persisted
|
||||
|
||||
return pojo;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BNoteAncillary;
|
@ -1,5 +1,6 @@
|
||||
const BNote = require('./entities/bnote');
|
||||
const BNoteRevision = require('./entities/bnote_revision');
|
||||
const BNoteAncillary = require("./entities/bnote_ancillary");
|
||||
const BBranch = require('./entities/bbranch');
|
||||
const BAttribute = require('./entities/battribute');
|
||||
const BRecentNote = require('./entities/brecent_note');
|
||||
@ -13,6 +14,8 @@ const ENTITY_NAME_TO_ENTITY = {
|
||||
"note_contents": BNote,
|
||||
"note_revisions": BNoteRevision,
|
||||
"note_revision_contents": BNoteRevision,
|
||||
"note_ancillaries": BNoteAncillary,
|
||||
"note_ancillary_contents": BNoteAncillary,
|
||||
"recent_notes": BRecentNote,
|
||||
"etapi_tokens": BEtapiToken,
|
||||
"options": BOption
|
||||
|
@ -36,7 +36,7 @@ async function processEntityChanges(entityChanges) {
|
||||
|
||||
loadResults.addOption(ec.entity.name);
|
||||
}
|
||||
else if (['etapi_tokens'].includes(ec.entityName)) {
|
||||
else if (['etapi_tokens', 'note_ancillaries', 'note_ancillary_contents'].includes(ec.entityName)) {
|
||||
// NOOP
|
||||
}
|
||||
else {
|
||||
|
@ -28,6 +28,7 @@ const TPL = `
|
||||
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> Re-render note</a>
|
||||
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
|
||||
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
|
||||
<a data-trigger-command="showNoteAncillaries" class="dropdown-item"><kbd data-command="showNoteAncillaries"></kbd> Note ancillaries</a>
|
||||
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
|
||||
<a class="dropdown-item import-files-button">Import files</a>
|
||||
<a class="dropdown-item export-note-button">Export note</a>
|
||||
|
@ -27,6 +27,7 @@ import NoteMapTypeWidget from "./type_widgets/note_map.js";
|
||||
import WebViewTypeWidget from "./type_widgets/web_view.js";
|
||||
import DocTypeWidget from "./type_widgets/doc.js";
|
||||
import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
|
||||
import AncillariesTypeWidget from "./type_widgets/ancillaries.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail">
|
||||
@ -61,7 +62,8 @@ const typeWidgetClasses = {
|
||||
'noteMap': NoteMapTypeWidget,
|
||||
'webView': WebViewTypeWidget,
|
||||
'doc': DocTypeWidget,
|
||||
'contentWidget': ContentWidgetTypeWidget
|
||||
'contentWidget': ContentWidgetTypeWidget,
|
||||
'ancillaries': AncillariesTypeWidget
|
||||
};
|
||||
|
||||
export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
@ -189,6 +191,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') {
|
||||
type = 'readOnlyCode';
|
||||
} else if (this.noteContext.viewScope.viewMode === 'ancillaries') {
|
||||
type = 'ancillaries';
|
||||
} else if (type === 'text' && await this.noteContext.isReadOnly()) {
|
||||
type = 'readOnlyText';
|
||||
} else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) {
|
||||
|
79
src/public/app/widgets/type_widgets/ancillaries.js
Normal file
79
src/public/app/widgets/type_widgets/ancillaries.js
Normal file
@ -0,0 +1,79 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-ancillaries note-detail-printable">
|
||||
<style>
|
||||
.note-ancillaries {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.ancillary-content {
|
||||
max-height: 400px;
|
||||
background: var(--accented-background-color);
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ancillary-details th {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="alert alert-info" style="margin: 10px 0 10px 0; padding: 20px;">
|
||||
Note ancillaries are pieces of data attached to a given note, providing ancillary support.
|
||||
This view is useful for diagnostics.
|
||||
</div>
|
||||
|
||||
<div class="note-ancillary-list"></div>
|
||||
</div>`;
|
||||
|
||||
export default class AncillariesTypeWidget extends TypeWidget {
|
||||
static getType() { return "ancillaries"; }
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$list = this.$widget.find('.note-ancillary-list');
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
this.$list.empty();
|
||||
|
||||
const ancillaries = await server.get(`notes/${this.noteId}/ancillaries?includeContent=true`);
|
||||
|
||||
if (ancillaries.length === 0) {
|
||||
this.$list.html("<strong>This note has no ancillaries.</strong>");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ancillary of ancillaries) {
|
||||
this.$list.append(
|
||||
$('<div class="note-ancillary-wrapper">')
|
||||
.append(
|
||||
$('<h4>').append($('<span class="ancillary-name">').text(ancillary.name))
|
||||
)
|
||||
.append(
|
||||
$('<table class="ancillary-details">')
|
||||
.append(
|
||||
$('<tr>')
|
||||
.append($('<th>').text('Length:'))
|
||||
.append($('<td>').text(ancillary.contentLength))
|
||||
.append($('<th>').text('MIME:'))
|
||||
.append($('<td>').text(ancillary.mime))
|
||||
.append($('<th>').text('Date modified:'))
|
||||
.append($('<td>').text(ancillary.utcDateModified))
|
||||
)
|
||||
)
|
||||
.append(
|
||||
$('<pre class="ancillary-content">')
|
||||
.text(ancillary.content)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -54,10 +54,10 @@ function createNote(req) {
|
||||
}
|
||||
|
||||
function updateNoteData(req) {
|
||||
const {content} = req.body;
|
||||
const {content, ancillaries} = req.body;
|
||||
const {noteId} = req.params;
|
||||
|
||||
return noteService.updateNoteData(noteId, content);
|
||||
return noteService.updateNoteData(noteId, content, ancillaries);
|
||||
}
|
||||
|
||||
function deleteNote(req) {
|
||||
@ -127,6 +127,49 @@ function setNoteTypeMime(req) {
|
||||
note.save();
|
||||
}
|
||||
|
||||
function getNoteAncillaries(req) {
|
||||
const includeContent = req.query.includeContent === 'true';
|
||||
const {noteId} = req.params;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
const noteAncillaries = note.getNoteAncillaries();
|
||||
|
||||
return noteAncillaries.map(ancillary => {
|
||||
const pojo = ancillary.getPojo();
|
||||
|
||||
if (includeContent && utils.isStringNote(null, ancillary.mime)) {
|
||||
pojo.content = ancillary.getContent()?.toString();
|
||||
pojo.contentLength = pojo.content.length;
|
||||
|
||||
const MAX_ANCILLARY_LENGTH = 1_000_000;
|
||||
|
||||
if (pojo.content.length > MAX_ANCILLARY_LENGTH) {
|
||||
pojo.content = pojo.content.substring(0, MAX_ANCILLARY_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
return pojo;
|
||||
});
|
||||
}
|
||||
|
||||
function saveNoteAncillary(req) {
|
||||
const {noteId, name} = req.params;
|
||||
const {mime, content} = req.body;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
|
||||
}
|
||||
|
||||
note.saveNoteAncillary(name, mime, content);
|
||||
}
|
||||
|
||||
function getRelationMap(req) {
|
||||
const {relationMapNoteId, noteIds} = req.body;
|
||||
|
||||
@ -340,5 +383,7 @@ module.exports = {
|
||||
eraseDeletedNotesNow,
|
||||
getDeleteNotesPreview,
|
||||
uploadModifiedFile,
|
||||
forceSaveNoteRevision
|
||||
forceSaveNoteRevision,
|
||||
getNoteAncillaries,
|
||||
saveNoteAncillary
|
||||
};
|
||||
|
@ -114,6 +114,14 @@ function forceNoteSync(req) {
|
||||
entityChangesService.moveEntityChangeToTop('note_revision_contents', noteRevisionId);
|
||||
}
|
||||
|
||||
for (const noteAncillaryId of sql.getColumn("SELECT noteAncillaryId FROM note_ancillaries WHERE noteId = ?", [noteId])) {
|
||||
sql.execute(`UPDATE note_ancillaries SET utcDateModified = ? WHERE noteAncillaryId = ?`, [now, noteAncillaryId]);
|
||||
entityChangesService.moveEntityChangeToTop('note_ancillaries', noteAncillaryId);
|
||||
|
||||
sql.execute(`UPDATE note_ancillary_contents SET utcDateModified = ? WHERE noteAncillaryId = ?`, [now, noteAncillaryId]);
|
||||
entityChangesService.moveEntityChangeToTop('note_ancillary_contents', noteAncillaryId);
|
||||
}
|
||||
|
||||
log.info(`Forcing note sync for ${noteId}`);
|
||||
|
||||
// not awaiting for the job to finish (will probably take a long time)
|
||||
|
@ -126,6 +126,8 @@ function register(app) {
|
||||
apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes);
|
||||
apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote);
|
||||
apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
|
||||
apiRoute(GET, '/api/notes/:noteId/ancillaries', notesApiRoute.getNoteAncillaries);
|
||||
apiRoute(PUT, '/api/notes/:noteId/ancillaries/:name', notesApiRoute.saveNoteAncillary);
|
||||
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
|
||||
apiRoute(DELETE, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
|
||||
apiRoute(GET, '/api/notes/:noteId/revisions/:noteRevisionId', noteRevisionsApiRoute.getNoteRevision);
|
||||
|
@ -4,8 +4,8 @@ const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
const {TRILIUM_DATA_DIR} = require('./data_dir');
|
||||
|
||||
const APP_DB_VERSION = 213;
|
||||
const SYNC_VERSION = 29;
|
||||
const APP_DB_VERSION = 214;
|
||||
const SYNC_VERSION = 30;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
module.exports = {
|
||||
|
@ -213,6 +213,25 @@ class ConsistencyChecks {
|
||||
logError(`Relation '${attributeId}' references missing note '${noteId}'`)
|
||||
}
|
||||
});
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT noteAncillaryId, note_ancillaries.noteId AS noteId
|
||||
FROM note_ancillaries
|
||||
LEFT JOIN notes USING (noteId)
|
||||
WHERE notes.noteId IS NULL
|
||||
AND note_ancillaries.isDeleted = 0`,
|
||||
({noteAncillaryId, noteId}) => {
|
||||
if (this.autoFix) {
|
||||
const noteAncillary = becca.getNoteAncillary(noteAncillaryId);
|
||||
noteAncillary.markAsDeleted();
|
||||
|
||||
this.reloadNeeded = false;
|
||||
|
||||
logFix(`Note ancillary '${noteAncillaryId}' has been deleted since it references missing note '${noteId}'`);
|
||||
} else {
|
||||
logError(`Note ancillary '${noteAncillaryId}' references missing note '${noteId}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findExistencyIssues() {
|
||||
@ -320,6 +339,26 @@ class ConsistencyChecks {
|
||||
logError(`Duplicate branches for note '${noteId}' and parent '${parentNoteId}'`);
|
||||
}
|
||||
});
|
||||
|
||||
this.findAndFixIssues(`
|
||||
SELECT noteAncillaryId,
|
||||
note_ancillaries.noteId AS noteId
|
||||
FROM note_ancillaries
|
||||
JOIN notes USING (noteId)
|
||||
WHERE notes.isDeleted = 1
|
||||
AND note_ancillaries.isDeleted = 0`,
|
||||
({noteAncillaryId, noteId}) => {
|
||||
if (this.autoFix) {
|
||||
const noteAncillary = becca.getNoteAncillary(noteAncillaryId);
|
||||
noteAncillary.markAsDeleted();
|
||||
|
||||
this.reloadNeeded = false;
|
||||
|
||||
logFix(`Note ancillary '${noteAncillaryId}' has been deleted since associated note '${noteId}' is deleted.`);
|
||||
} else {
|
||||
logError(`Note ancillary '${noteAncillaryId}' is not deleted even though associated note '${noteId}' is deleted.`)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findLogicIssues() {
|
||||
@ -620,6 +659,8 @@ class ConsistencyChecks {
|
||||
this.runEntityChangeChecks("note_contents", "noteId");
|
||||
this.runEntityChangeChecks("note_revisions", "noteRevisionId");
|
||||
this.runEntityChangeChecks("note_revision_contents", "noteRevisionId");
|
||||
this.runEntityChangeChecks("note_ancillaries", "noteAncillaryId");
|
||||
this.runEntityChangeChecks("note_ancillary_contents", "noteAncillaryId");
|
||||
this.runEntityChangeChecks("branches", "branchId");
|
||||
this.runEntityChangeChecks("attributes", "attributeId");
|
||||
this.runEntityChangeChecks("etapi_tokens", "etapiTokenId");
|
||||
@ -715,7 +756,7 @@ class ConsistencyChecks {
|
||||
return `${tableName}: ${count}`;
|
||||
}
|
||||
|
||||
const tables = [ "notes", "note_revisions", "branches", "attributes", "etapi_tokens" ];
|
||||
const tables = [ "notes", "note_revisions", "note_ancillaries", "branches", "attributes", "etapi_tokens" ];
|
||||
|
||||
log.info(`Table counts: ${tables.map(tableName => getTableRowCount(tableName)).join(", ")}`);
|
||||
}
|
||||
|
@ -151,6 +151,8 @@ function fillAllEntityChanges() {
|
||||
fillEntityChanges("branches", "branchId");
|
||||
fillEntityChanges("note_revisions", "noteRevisionId");
|
||||
fillEntityChanges("note_revision_contents", "noteRevisionId");
|
||||
fillEntityChanges("note_ancillaries", "noteAncillaryId");
|
||||
fillEntityChanges("note_ancillary_contents", "noteAncillaryId");
|
||||
fillEntityChanges("attributes", "attributeId");
|
||||
fillEntityChanges("etapi_tokens", "etapiTokenId");
|
||||
fillEntityChanges("options", "name", 'isSynced = 1');
|
||||
|
@ -170,6 +170,24 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
|
||||
meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames);
|
||||
}
|
||||
|
||||
const ancillaries = note.getNoteAncillaries();
|
||||
|
||||
if (ancillaries.length > 0) {
|
||||
meta.ancillaries = ancillaries
|
||||
.filter(ancillary => ["canvasSvg", "mermaidSvg"].includes(ancillary.name))
|
||||
.map(ancillary => ({
|
||||
|
||||
name: ancillary.name,
|
||||
mime: ancillary.mime,
|
||||
dataFileName: getDataFileName(
|
||||
null,
|
||||
ancillary.mime,
|
||||
baseFileName + "_" + ancillary.name,
|
||||
existingFileNames
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
if (childBranches.length > 0) {
|
||||
meta.dirFileName = getUniqueFilename(existingFileNames, baseFileName);
|
||||
meta.children = [];
|
||||
@ -319,6 +337,16 @@ ${markdownContent}`;
|
||||
|
||||
taskContext.increaseProgressCount();
|
||||
|
||||
for (const ancillaryMeta of noteMeta.ancillaries || []) {
|
||||
const noteAncillary = note.getNoteAncillaryByName(ancillaryMeta.name);
|
||||
const content = noteAncillary.getContent();
|
||||
|
||||
archive.append(content, {
|
||||
name: filePathPrefix + ancillaryMeta.dataFileName,
|
||||
date: dateUtils.parseDateTime(note.utcDateModified)
|
||||
});
|
||||
}
|
||||
|
||||
if (noteMeta.children && noteMeta.children.length > 0) {
|
||||
const directoryPath = filePathPrefix + noteMeta.dirFileName;
|
||||
|
||||
|
@ -14,6 +14,7 @@ const treeService = require("../tree");
|
||||
const yauzl = require("yauzl");
|
||||
const htmlSanitizer = require('../html_sanitizer');
|
||||
const becca = require("../../becca/becca");
|
||||
const BNoteAncillary = require("../../becca/entities/bnote_ancillary");
|
||||
|
||||
/**
|
||||
* @param {TaskContext} taskContext
|
||||
@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
};
|
||||
|
||||
let parent;
|
||||
let ancillaryMeta = false;
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
if (!cursor || !cursor.children || cursor.children.length === 0) {
|
||||
@ -72,11 +74,28 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
|
||||
parent = cursor;
|
||||
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
|
||||
|
||||
if (!cursor) {
|
||||
for (const file of parent.children) {
|
||||
for (const ancillary of file.ancillaries || []) {
|
||||
if (ancillary.dataFileName === segment) {
|
||||
cursor = file;
|
||||
ancillaryMeta = ancillary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parentNoteMeta: parent,
|
||||
noteMeta: cursor
|
||||
noteMeta: cursor,
|
||||
ancillaryMeta
|
||||
};
|
||||
}
|
||||
|
||||
@ -351,7 +370,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
}
|
||||
|
||||
function saveNote(filePath, content) {
|
||||
const {parentNoteMeta, noteMeta} = getMeta(filePath);
|
||||
const {parentNoteMeta, noteMeta, ancillaryMeta} = getMeta(filePath);
|
||||
|
||||
if (noteMeta?.noImport) {
|
||||
return;
|
||||
@ -359,6 +378,17 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
|
||||
const noteId = getNoteId(noteMeta, filePath);
|
||||
|
||||
if (ancillaryMeta) {
|
||||
const noteAncillary = new BNoteAncillary({
|
||||
noteId,
|
||||
name: ancillaryMeta.name,
|
||||
mime: ancillaryMeta.mime
|
||||
});
|
||||
|
||||
noteAncillary.setContent(content);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
|
||||
|
||||
if (!parentNoteId) {
|
||||
|
37
src/services/note_ancillaries.js
Normal file
37
src/services/note_ancillaries.js
Normal file
@ -0,0 +1,37 @@
|
||||
const protectedSession = require("./protected_session");
|
||||
const log = require("./log");
|
||||
|
||||
/**
|
||||
* @param {BNote} note
|
||||
*/
|
||||
function protectNoteAncillaries(note) {
|
||||
for (const noteAncillary of note.getNoteAncillaries()) {
|
||||
if (note.isProtected !== noteAncillary.isProtected) {
|
||||
if (!protectedSession.isProtectedSessionAvailable()) {
|
||||
log.error("Protected session is not available to fix note ancillaries.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = noteAncillary.getContent();
|
||||
|
||||
noteAncillary.isProtected = note.isProtected;
|
||||
|
||||
// this will force de/encryption
|
||||
noteAncillary.setContent(content);
|
||||
|
||||
noteAncillary.save();
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Could not un/protect note ancillary ID = ${noteAncillary.noteAncillaryId}`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
protectNoteAncillaries
|
||||
}
|
@ -321,7 +321,7 @@ function getEntityChangeRow(entityName, entityId) {
|
||||
throw new Error(`Entity ${entityName} ${entityId} not found.`);
|
||||
}
|
||||
|
||||
if (['note_contents', 'note_revision_contents'].includes(entityName) && entity.content !== null) {
|
||||
if (['note_contents', 'note_revision_contents', 'note_ancillary_contents'].includes(entityName) && entity.content !== null) {
|
||||
if (typeof entity.content === 'string') {
|
||||
entity.content = Buffer.from(entity.content, 'UTF-8');
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ function updateNormalEntity(remoteEntityChange, remoteEntityRow, instanceId) {
|
||||
|| localEntityChange.utcDateChanged < remoteEntityChange.utcDateChanged
|
||||
|| localEntityChange.hash !== remoteEntityChange.hash // sync error, we should still update
|
||||
) {
|
||||
if (['note_contents', 'note_revision_contents'].includes(remoteEntityChange.entityName)) {
|
||||
if (['note_contents', 'note_revision_contents', 'note_ancillary_contents'].includes(remoteEntityChange.entityName)) {
|
||||
remoteEntityRow.content = handleContent(remoteEntityRow.content);
|
||||
}
|
||||
|
||||
@ -115,7 +115,9 @@ function eraseEntity(entityChange, instanceId) {
|
||||
"branches",
|
||||
"attributes",
|
||||
"note_revisions",
|
||||
"note_revision_contents"
|
||||
"note_revision_contents",
|
||||
"note_ancillaries",
|
||||
"note_ancillary_contents"
|
||||
];
|
||||
|
||||
if (!entityNames.includes(entityName)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user