export/import attachments

This commit is contained in:
zadam 2023-01-25 09:55:29 +01:00
parent 0bfb2631df
commit bd8568809f
5 changed files with 106 additions and 18 deletions

View file

@ -2,6 +2,6 @@
SCHEMA_FILE_PATH=db/schema.sql
sqlite3 ~/trilium-data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
sqlite3 ./data/document.db .schema | grep -v "sqlite_sequence" > "$SCHEMA_FILE_PATH"
echo "DB schema exported to $SCHEMA_FILE_PATH"

View file

@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIM
`dateLastEdited` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT DEFAULT NULL,
`content` TEXT,
`utcDateModified` TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS "options"
(
@ -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_attachments"
(
noteAttachmentId 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_attachment_contents" (`noteAttachmentId` TEXT NOT NULL PRIMARY KEY,
`content` TEXT DEFAULT NULL,
`utcDateModified` TEXT NOT NULL);
CREATE INDEX IDX_note_attachments_name
on note_attachments (name);
CREATE UNIQUE INDEX IDX_note_attachments_noteId_name
on note_attachments (noteId, name);

View file

@ -91,7 +91,7 @@ class BNoteAttachment extends AbstractBeccaEntity {
setContent(content) {
this.contentCheckSum = this.calculateCheckSum(content);
this.save();
this.save(); // also explicitly save note_attachment to update contentCheckSum
const pojo = {
noteAttachmentId: this.noteAttachmentId,

View file

@ -58,7 +58,7 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
}
}
function getDataFileName(note, baseFileName, existingFileNames) {
function getDataFileName(type, mime, baseFileName, existingFileNames) {
let fileName = baseFileName;
let existingExtension = path.extname(fileName).toLowerCase();
@ -70,24 +70,25 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
// following two are handled specifically since we always want to have these extensions no matter the automatic detection
// and/or existing detected extensions in the note name
if (note.type === 'text' && format === 'markdown') {
if (type === 'text' && format === 'markdown') {
newExtension = 'md';
}
else if (note.type === 'text' && format === 'html') {
else if (type === 'text' && format === 'html') {
newExtension = 'html';
}
else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') {
else if (mime === 'application/x-javascript' || mime === 'text/javascript') {
newExtension = 'js';
}
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
newExtension = null;
}
else {
if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
if (mime?.toLowerCase()?.trim() === "image/jpg") {
newExtension = 'jpg';
}
else {
newExtension = mimeTypes.extension(note.mime) || "dat";
} else if (mime?.toLowerCase()?.trim() === "text/mermaid") {
newExtension = 'txt';
} else {
newExtension = mimeTypes.extension(mime) || "dat";
}
}
@ -166,7 +167,25 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
// if it's a leaf then we'll export it even if it's empty
if (available && (note.getContent().length > 0 || childBranches.length === 0)) {
meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
meta.dataFileName = getDataFileName(note.type, note.mime, baseFileName, existingFileNames);
}
const attachments = note.getNoteAttachments();
if (attachments.length > 0) {
meta.attachments = attachments
.filter(attachment => ["canvasSvg", "mermaidSvg"].includes(attachment.name))
.map(attachment => ({
name: attachment.name,
mime: attachment.mime,
dataFileName: getDataFileName(
null,
attachment.mime,
baseFileName + "_" + attachment.name,
existingFileNames
)
}));
}
if (childBranches.length > 0) {
@ -215,8 +234,15 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
// link can target note which is only "folder-note" and as such will not have a file in an export
url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
// for some note types it's more user-friendly to see the attachment (if exists) instead of source note
const preferredAttachment = (meta.attachments || []).find(attachment => ['mermaidSvg', 'canvasSvg'].includes(attachment.name));
if (preferredAttachment) {
url += encodeURIComponent(preferredAttachment.dataFileName);
} else {
// link can target note which is only "folder-note" and as such will not have a file in an export
url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
}
return url;
}
@ -310,11 +336,24 @@ ${markdownContent}`;
if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
archive.append(content, { name: filePathPrefix + noteMeta.dataFileName, date: dateUtils.parseDateTime(note.utcDateModified) });
archive.append(content, {
name: filePathPrefix + noteMeta.dataFileName,
date: dateUtils.parseDateTime(note.utcDateModified)
});
}
taskContext.increaseProgressCount();
for (const attachmentMeta of noteMeta.attachments || []) {
const noteAttachment = note.getNoteAttachmentByName(attachmentMeta.name);
const content = noteAttachment.getContent();
archive.append(content, {
name: filePathPrefix + attachmentMeta.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 BNoteAttachment = require("../../becca/entities/bnote_attachment");
/**
* @param {TaskContext} taskContext
@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
};
let parent;
let attachmentMeta = false;
for (const segment of pathSegments) {
if (!cursor || !cursor.children || cursor.children.length === 0) {
@ -71,12 +73,29 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
parent = cursor;
cursor = cursor.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
cursor = parent.children.find(file => file.dataFileName === segment || file.dirFileName === segment);
if (!cursor) {
for (const file of parent.children) {
for (const attachment of file.attachments || []) {
if (attachment.dataFileName === segment) {
cursor = file;
attachmentMeta = attachment;
break;
}
}
if (cursor) {
break;
}
}
}
}
return {
parentNoteMeta: parent,
noteMeta: cursor
noteMeta: cursor,
attachmentMeta
};
}
@ -354,13 +373,25 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
function saveNote(filePath, content) {
const {parentNoteMeta, noteMeta} = getMeta(filePath);
const {parentNoteMeta, noteMeta, attachmentMeta} = getMeta(filePath);
if (noteMeta?.noImport) {
return;
}
const noteId = getNoteId(noteMeta, filePath);
if (attachmentMeta) {
const noteAttachment = new BNoteAttachment({
noteId,
name: attachmentMeta.name,
mime: attachmentMeta.mime
});
noteAttachment.setContent(content);
return;
}
const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
if (!parentNoteId) {