note ancillary/attachment backported from dev branch

This commit is contained in:
zadam 2023-03-08 09:01:23 +01:00
parent 6b4800d2d6
commit 929f8ef720
25 changed files with 890 additions and 355 deletions

View File

@ -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'

View 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);

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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,

View File

@ -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;

View File

@ -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();

View 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;

View File

@ -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

View File

@ -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 {

View File

@ -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>

View File

@ -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()) {

View 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)
)
);
}
}
}

View File

@ -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
};

View File

@ -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)

View File

@ -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);

View File

@ -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 = {

View File

@ -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(", ")}`);
}

View File

@ -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');

View File

@ -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;

View File

@ -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) {

View 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
}

View File

@ -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');
}

View File

@ -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)) {