mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
export/import attachments
This commit is contained in:
parent
0bfb2631df
commit
bd8568809f
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
SCHEMA_FILE_PATH=db/schema.sql
|
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"
|
echo "DB schema exported to $SCHEMA_FILE_PATH"
|
@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (`noteRevisionId` TEXT NOT NULL PRIM
|
|||||||
`dateLastEdited` TEXT NOT NULL,
|
`dateLastEdited` TEXT NOT NULL,
|
||||||
`dateCreated` TEXT NOT NULL);
|
`dateCreated` TEXT NOT NULL);
|
||||||
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
CREATE TABLE IF NOT EXISTS "note_revision_contents" (`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
|
||||||
`content` TEXT DEFAULT NULL,
|
`content` TEXT,
|
||||||
`utcDateModified` TEXT NOT NULL);
|
`utcDateModified` TEXT NOT NULL);
|
||||||
CREATE TABLE IF NOT EXISTS "options"
|
CREATE TABLE IF NOT EXISTS "options"
|
||||||
(
|
(
|
||||||
@ -112,3 +112,21 @@ CREATE TABLE IF NOT EXISTS "recent_notes"
|
|||||||
notePath TEXT not null,
|
notePath TEXT not null,
|
||||||
utcDateCreated 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);
|
||||||
|
@ -91,7 +91,7 @@ class BNoteAttachment extends AbstractBeccaEntity {
|
|||||||
|
|
||||||
setContent(content) {
|
setContent(content) {
|
||||||
this.contentCheckSum = this.calculateCheckSum(content);
|
this.contentCheckSum = this.calculateCheckSum(content);
|
||||||
this.save();
|
this.save(); // also explicitly save note_attachment to update contentCheckSum
|
||||||
|
|
||||||
const pojo = {
|
const pojo = {
|
||||||
noteAttachmentId: this.noteAttachmentId,
|
noteAttachmentId: this.noteAttachmentId,
|
||||||
|
@ -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 fileName = baseFileName;
|
||||||
|
|
||||||
let existingExtension = path.extname(fileName).toLowerCase();
|
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
|
// 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
|
// and/or existing detected extensions in the note name
|
||||||
if (note.type === 'text' && format === 'markdown') {
|
if (type === 'text' && format === 'markdown') {
|
||||||
newExtension = 'md';
|
newExtension = 'md';
|
||||||
}
|
}
|
||||||
else if (note.type === 'text' && format === 'html') {
|
else if (type === 'text' && format === 'html') {
|
||||||
newExtension = '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';
|
newExtension = 'js';
|
||||||
}
|
}
|
||||||
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
|
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
|
||||||
newExtension = null;
|
newExtension = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
|
if (mime?.toLowerCase()?.trim() === "image/jpg") {
|
||||||
newExtension = 'jpg';
|
newExtension = 'jpg';
|
||||||
}
|
} else if (mime?.toLowerCase()?.trim() === "text/mermaid") {
|
||||||
else {
|
newExtension = 'txt';
|
||||||
newExtension = mimeTypes.extension(note.mime) || "dat";
|
} 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 it's a leaf then we'll export it even if it's empty
|
||||||
if (available && (note.getContent().length > 0 || childBranches.length === 0)) {
|
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) {
|
if (childBranches.length > 0) {
|
||||||
@ -215,8 +234,15 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
|
|||||||
|
|
||||||
const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
|
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
|
// for some note types it's more user-friendly to see the attachment (if exists) instead of source note
|
||||||
url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
|
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;
|
return url;
|
||||||
}
|
}
|
||||||
@ -310,11 +336,24 @@ ${markdownContent}`;
|
|||||||
if (noteMeta.dataFileName) {
|
if (noteMeta.dataFileName) {
|
||||||
const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
|
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();
|
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) {
|
if (noteMeta.children && noteMeta.children.length > 0) {
|
||||||
const directoryPath = filePathPrefix + noteMeta.dirFileName;
|
const directoryPath = filePathPrefix + noteMeta.dirFileName;
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ const treeService = require("../tree");
|
|||||||
const yauzl = require("yauzl");
|
const yauzl = require("yauzl");
|
||||||
const htmlSanitizer = require('../html_sanitizer');
|
const htmlSanitizer = require('../html_sanitizer');
|
||||||
const becca = require("../../becca/becca");
|
const becca = require("../../becca/becca");
|
||||||
|
const BNoteAttachment = require("../../becca/entities/bnote_attachment");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TaskContext} taskContext
|
* @param {TaskContext} taskContext
|
||||||
@ -64,6 +65,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let parent;
|
let parent;
|
||||||
|
let attachmentMeta = false;
|
||||||
|
|
||||||
for (const segment of pathSegments) {
|
for (const segment of pathSegments) {
|
||||||
if (!cursor || !cursor.children || cursor.children.length === 0) {
|
if (!cursor || !cursor.children || cursor.children.length === 0) {
|
||||||
@ -71,12 +73,29 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parent = cursor;
|
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 {
|
return {
|
||||||
parentNoteMeta: parent,
|
parentNoteMeta: parent,
|
||||||
noteMeta: cursor
|
noteMeta: cursor,
|
||||||
|
attachmentMeta
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,13 +373,25 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveNote(filePath, content) {
|
function saveNote(filePath, content) {
|
||||||
const {parentNoteMeta, noteMeta} = getMeta(filePath);
|
const {parentNoteMeta, noteMeta, attachmentMeta} = getMeta(filePath);
|
||||||
|
|
||||||
if (noteMeta?.noImport) {
|
if (noteMeta?.noImport) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteId = getNoteId(noteMeta, filePath);
|
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);
|
const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
|
||||||
|
|
||||||
if (!parentNoteId) {
|
if (!parentNoteId) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user