mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
added ImportContext
This commit is contained in:
parent
5baa251944
commit
e4c78f3887
@ -1,4 +1,5 @@
|
|||||||
import treeService from '../services/tree.js';
|
import treeService from '../services/tree.js';
|
||||||
|
import utils from '../services/utils.js';
|
||||||
import treeUtils from "../services/tree_utils.js";
|
import treeUtils from "../services/tree_utils.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import infoService from "../services/info.js";
|
import infoService from "../services/info.js";
|
||||||
@ -12,7 +13,11 @@ const $importNoteCountWrapper = $("#import-note-count-wrapper");
|
|||||||
const $importNoteCount = $("#import-note-count");
|
const $importNoteCount = $("#import-note-count");
|
||||||
const $importButton = $("#import-button");
|
const $importButton = $("#import-button");
|
||||||
|
|
||||||
|
let importId;
|
||||||
|
|
||||||
async function showDialog() {
|
async function showDialog() {
|
||||||
|
// each opening of the dialog resets the importId so we don't associate it with previous imports anymore
|
||||||
|
importId = '';
|
||||||
$importNoteCountWrapper.hide();
|
$importNoteCountWrapper.hide();
|
||||||
$importNoteCount.text('0');
|
$importNoteCount.text('0');
|
||||||
$fileUploadInput.val('').change(); // to trigger Import button disabling listener below
|
$fileUploadInput.val('').change(); // to trigger Import button disabling listener below
|
||||||
@ -40,8 +45,12 @@ function importIntoNote(importNoteId) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('upload', $fileUploadInput[0].files[0]);
|
formData.append('upload', $fileUploadInput[0].files[0]);
|
||||||
|
|
||||||
|
// we generate it here (and not on opening) for the case when you try to import multiple times from the same
|
||||||
|
// dialog (which shouldn't happen, but still ...)
|
||||||
|
importId = utils.randomString(10);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: baseApiUrl + 'notes/' + importNoteId + '/import',
|
url: baseApiUrl + 'notes/' + importNoteId + '/import/' + importId,
|
||||||
headers: server.getHeaders(),
|
headers: server.getHeaders(),
|
||||||
data: formData,
|
data: formData,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
@ -54,6 +63,11 @@ function importIntoNote(importNoteId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messagingService.subscribeToMessages(async message => {
|
messagingService.subscribeToMessages(async message => {
|
||||||
|
if (!message.importId || message.importId !== importId) {
|
||||||
|
// incoming messages must correspond to this import instance
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === 'import-note-count') {
|
if (message.type === 'import-note-count') {
|
||||||
$importNoteCountWrapper.show();
|
$importNoteCountWrapper.show();
|
||||||
|
|
||||||
|
@ -5,12 +5,45 @@ const enexImportService = require('../../services/import/enex');
|
|||||||
const opmlImportService = require('../../services/import/opml');
|
const opmlImportService = require('../../services/import/opml');
|
||||||
const tarImportService = require('../../services/import/tar');
|
const tarImportService = require('../../services/import/tar');
|
||||||
const singleImportService = require('../../services/import/single');
|
const singleImportService = require('../../services/import/single');
|
||||||
|
const messagingService = require('../../services/messaging');
|
||||||
const cls = require('../../services/cls');
|
const cls = require('../../services/cls');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const noteCacheService = require('../../services/note_cache');
|
const noteCacheService = require('../../services/note_cache');
|
||||||
|
|
||||||
|
class ImportContext {
|
||||||
|
constructor(importId) {
|
||||||
|
// importId is to distinguish between different import events - it is possible (though not recommended)
|
||||||
|
// to have multiple imports going at the same time
|
||||||
|
this.importId = importId;
|
||||||
|
this.count = 0;
|
||||||
|
this.lastSentCountTs = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
async increaseCount() {
|
||||||
|
this.count++;
|
||||||
|
|
||||||
|
if (Date.now() - this.lastSentCountTs >= 1000) {
|
||||||
|
this.lastSentCountTs = Date.now();
|
||||||
|
|
||||||
|
await messagingService.sendMessageToAllClients({
|
||||||
|
importId: this.importId,
|
||||||
|
type: 'import-note-count',
|
||||||
|
count: this.count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importFinished(noteId) {
|
||||||
|
await messagingService.sendMessageToAllClients({
|
||||||
|
importId: this.importId,
|
||||||
|
type: 'import-finished',
|
||||||
|
noteId: noteId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function importToBranch(req) {
|
async function importToBranch(req) {
|
||||||
const parentNoteId = req.params.parentNoteId;
|
const {parentNoteId, importId} = req.params;
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@ -31,20 +64,22 @@ async function importToBranch(req) {
|
|||||||
|
|
||||||
let note; // typically root of the import - client can show it after finishing the import
|
let note; // typically root of the import - client can show it after finishing the import
|
||||||
|
|
||||||
|
const importContext = new ImportContext(importId);
|
||||||
|
|
||||||
if (extension === '.tar') {
|
if (extension === '.tar') {
|
||||||
note = await tarImportService.importTar(file.buffer, parentNote);
|
note = await tarImportService.importTar(importContext, file.buffer, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.opml') {
|
else if (extension === '.opml') {
|
||||||
note = await opmlImportService.importOpml(file.buffer, parentNote);
|
note = await opmlImportService.importOpml(importContext, file.buffer, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.md') {
|
else if (extension === '.md') {
|
||||||
note = await singleImportService.importMarkdown(file, parentNote);
|
note = await singleImportService.importMarkdown(importContext, file, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.html' || extension === '.htm') {
|
else if (extension === '.html' || extension === '.htm') {
|
||||||
note = await singleImportService.importHtml(file, parentNote);
|
note = await singleImportService.importHtml(importContext, file, parentNote);
|
||||||
}
|
}
|
||||||
else if (extension === '.enex') {
|
else if (extension === '.enex') {
|
||||||
note = await enexImportService.importEnex(file, parentNote);
|
note = await enexImportService.importEnex(importContext, file, parentNote);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
|
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
|
||||||
|
@ -129,7 +129,7 @@ function register(app) {
|
|||||||
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
|
||||||
|
|
||||||
route(GET, '/api/notes/:branchId/export/:type/:format', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
route(GET, '/api/notes/:branchId/export/:type/:format', [auth.checkApiAuthOrElectron], exportRoute.exportBranch);
|
||||||
route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler);
|
route(POST, '/api/notes/:parentNoteId/import/:importId', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler);
|
||||||
|
|
||||||
route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware],
|
route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware],
|
||||||
filesRoute.uploadFile, apiResultHandler);
|
filesRoute.uploadFile, apiResultHandler);
|
||||||
|
@ -19,7 +19,7 @@ function parseDate(text) {
|
|||||||
let note = {};
|
let note = {};
|
||||||
let resource;
|
let resource;
|
||||||
|
|
||||||
async function importEnex(file, parentNote) {
|
async function importEnex(importContext, file, parentNote) {
|
||||||
const saxStream = sax.createStream(true);
|
const saxStream = sax.createStream(true);
|
||||||
const xmlBuilder = new xml2js.Builder({ headless: true });
|
const xmlBuilder = new xml2js.Builder({ headless: true });
|
||||||
const parser = new xml2js.Parser({ explicitArray: true });
|
const parser = new xml2js.Parser({ explicitArray: true });
|
||||||
@ -218,6 +218,8 @@ async function importEnex(file, parentNote) {
|
|||||||
mime: 'text/html'
|
mime: 'text/html'
|
||||||
})).note;
|
})).note;
|
||||||
|
|
||||||
|
importContext.increaseCount();
|
||||||
|
|
||||||
const noteContent = await noteEntity.getNoteContent();
|
const noteContent = await noteEntity.getNoteContent();
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
@ -232,39 +234,40 @@ async function importEnex(file, parentNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createResourceNote = async () => {
|
const createResourceNote = async () => {
|
||||||
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
|
const resourceNote = (await noteService.createNote(noteEntity.noteId, resource.title, resource.content, {
|
||||||
attributes: resource.attributes,
|
attributes: resource.attributes,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
mime: resource.mime
|
mime: resource.mime
|
||||||
})).note;
|
})).note;
|
||||||
|
|
||||||
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
importContext.increaseCount();
|
||||||
|
|
||||||
noteContent.content = noteContent.content.replace(mediaRegex, resourceLink);
|
const resourceLink = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`;
|
||||||
|
|
||||||
|
noteContent.content = noteContent.content.replace(mediaRegex, resourceLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
|
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) {
|
||||||
try {
|
try {
|
||||||
const originalName = "image." + resource.mime.substr(6);
|
const originalName = "image." + resource.mime.substr(6);
|
||||||
|
|
||||||
const { url } = await imageService.saveImage(resource.content, originalName, noteEntity.noteId);
|
const {url} = await imageService.saveImage(resource.content, originalName, noteEntity.noteId);
|
||||||
|
|
||||||
const imageLink = `<img src="${url}">`;
|
const imageLink = `<img src="${url}">`;
|
||||||
|
|
||||||
noteContent.content = noteContent.content.replace(mediaRegex, imageLink);
|
noteContent.content = noteContent.content.replace(mediaRegex, imageLink);
|
||||||
|
|
||||||
if (!noteContent.content.includes(imageLink)) {
|
if (!noteContent.content.includes(imageLink)) {
|
||||||
// if there wasn't any match for the reference, we'll add the image anyway
|
// if there wasn't any match for the reference, we'll add the image anyway
|
||||||
// otherwise image would be removed since no note would include it
|
// otherwise image would be removed since no note would include it
|
||||||
noteContent.content += imageLink;
|
noteContent.content += imageLink;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error("error when saving image from ENEX file: " + e);
|
||||||
|
await createResourceNote();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} else {
|
||||||
log.error("error when saving image from ENEX file: " + e);
|
|
||||||
await createResourceNote();
|
await createResourceNote();
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await createResourceNote();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +298,12 @@ async function importEnex(file, parentNote) {
|
|||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
// resolve only when we parse the whole document AND saving of all notes have been finished
|
// resolve only when we parse the whole document AND saving of all notes have been finished
|
||||||
saxStream.on("end", () => { Promise.all(saveNotePromises).then(() => resolve(rootNote)) });
|
saxStream.on("end", () => { Promise.all(saveNotePromises).then(() => {
|
||||||
|
importContext.importFinished(rootNote.noteId);
|
||||||
|
|
||||||
|
resolve(rootNote);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const bufferStream = new stream.PassThrough();
|
const bufferStream = new stream.PassThrough();
|
||||||
bufferStream.end(file.buffer);
|
bufferStream.end(file.buffer);
|
||||||
|
@ -3,7 +3,13 @@
|
|||||||
const noteService = require('../../services/notes');
|
const noteService = require('../../services/notes');
|
||||||
const parseString = require('xml2js').parseString;
|
const parseString = require('xml2js').parseString;
|
||||||
|
|
||||||
async function importOpml(fileBuffer, parentNote) {
|
/**
|
||||||
|
* @param {ImportContext} importContext
|
||||||
|
* @param {Buffer} fileBuffer
|
||||||
|
* @param {Note} parentNote
|
||||||
|
* @return {Promise<*[]|*>}
|
||||||
|
*/
|
||||||
|
async function importOpml(importContext, fileBuffer, parentNote) {
|
||||||
const xml = await new Promise(function(resolve, reject)
|
const xml = await new Promise(function(resolve, reject)
|
||||||
{
|
{
|
||||||
parseString(fileBuffer, function (err, result) {
|
parseString(fileBuffer, function (err, result) {
|
||||||
@ -24,7 +30,7 @@ async function importOpml(fileBuffer, parentNote) {
|
|||||||
let returnNote = null;
|
let returnNote = null;
|
||||||
|
|
||||||
for (const outline of outlines) {
|
for (const outline of outlines) {
|
||||||
const note = await importOutline(outline, parentNote.noteId);
|
const note = await importOutline(importContext, outline, parentNote.noteId);
|
||||||
|
|
||||||
// first created note will be activated after import
|
// first created note will be activated after import
|
||||||
returnNote = returnNote || note;
|
returnNote = returnNote || note;
|
||||||
@ -41,9 +47,11 @@ function toHtml(text) {
|
|||||||
return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>';
|
return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importOutline(outline, parentNoteId) {
|
async function importOutline(importContext, outline, parentNoteId) {
|
||||||
const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text));
|
const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text));
|
||||||
|
|
||||||
|
importContext.increaseCount();
|
||||||
|
|
||||||
for (const childOutline of (outline.outline || [])) {
|
for (const childOutline of (outline.outline || [])) {
|
||||||
await importOutline(childOutline, note.noteId);
|
await importOutline(childOutline, note.noteId);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ const noteService = require('../../services/notes');
|
|||||||
const commonmark = require('commonmark');
|
const commonmark = require('commonmark');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
async function importMarkdown(file, parentNote) {
|
async function importMarkdown(importContext, file, parentNote) {
|
||||||
const markdownContent = file.buffer.toString("UTF-8");
|
const markdownContent = file.buffer.toString("UTF-8");
|
||||||
|
|
||||||
const reader = new commonmark.Parser();
|
const reader = new commonmark.Parser();
|
||||||
@ -20,10 +20,13 @@ async function importMarkdown(file, parentNote) {
|
|||||||
mime: 'text/html'
|
mime: 'text/html'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
importContext.increaseCount();
|
||||||
|
importContext.importFinished(note.noteId);
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importHtml(file, parentNote) {
|
async function importHtml(importContext, file, parentNote) {
|
||||||
const title = getFileNameWithoutExtension(file.originalname);
|
const title = getFileNameWithoutExtension(file.originalname);
|
||||||
const content = file.buffer.toString("UTF-8");
|
const content = file.buffer.toString("UTF-8");
|
||||||
|
|
||||||
@ -32,6 +35,9 @@ async function importHtml(file, parentNote) {
|
|||||||
mime: 'text/html'
|
mime: 'text/html'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
importContext.increaseCount();
|
||||||
|
importContext.importFinished(note.noteId);
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ const utils = require('../../services/utils');
|
|||||||
const log = require('../../services/log');
|
const log = require('../../services/log');
|
||||||
const repository = require('../../services/repository');
|
const repository = require('../../services/repository');
|
||||||
const noteService = require('../../services/notes');
|
const noteService = require('../../services/notes');
|
||||||
const messagingService = require('../../services/messaging');
|
|
||||||
const Branch = require('../../entities/branch');
|
const Branch = require('../../entities/branch');
|
||||||
const tar = require('tar-stream');
|
const tar = require('tar-stream');
|
||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
@ -17,7 +16,13 @@ const mimeTypes = require('mime-types');
|
|||||||
let importNoteCount;
|
let importNoteCount;
|
||||||
let lastSentCountTs = Date.now();
|
let lastSentCountTs = Date.now();
|
||||||
|
|
||||||
async function importTar(fileBuffer, importRootNote) {
|
/**
|
||||||
|
* @param {ImportContext} importContext
|
||||||
|
* @param {Buffer} fileBuffer
|
||||||
|
* @param {Note} importRootNote
|
||||||
|
* @return {Promise<*>}
|
||||||
|
*/
|
||||||
|
async function importTar(importContext, fileBuffer, importRootNote) {
|
||||||
importNoteCount = 0;
|
importNoteCount = 0;
|
||||||
|
|
||||||
// maps from original noteId (in tar file) to newly generated noteId
|
// maps from original noteId (in tar file) to newly generated noteId
|
||||||
@ -337,13 +342,7 @@ async function importTar(fileBuffer, importRootNote) {
|
|||||||
log.info("Ignoring tar import entry with type " + header.type);
|
log.info("Ignoring tar import entry with type " + header.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
importNoteCount++;
|
importContext.increaseCount();
|
||||||
|
|
||||||
if (Date.now() - lastSentCountTs >= 1000) {
|
|
||||||
lastSentCountTs = Date.now();
|
|
||||||
|
|
||||||
messagingService.importNoteCount(importNoteCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
next(); // ready for next entry
|
next(); // ready for next entry
|
||||||
});
|
});
|
||||||
@ -379,7 +378,7 @@ async function importTar(fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messagingService.importFinished(firstNote);
|
importContext.importFinished();
|
||||||
|
|
||||||
resolve(firstNote);
|
resolve(firstNote);
|
||||||
});
|
});
|
||||||
|
@ -78,18 +78,8 @@ async function refreshTree() {
|
|||||||
await sendMessageToAllClients({ type: 'refresh-tree' });
|
await sendMessageToAllClients({ type: 'refresh-tree' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importNoteCount(count) {
|
|
||||||
await sendMessageToAllClients({ type: 'import-note-count', count: count });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importFinished(firstNote) {
|
|
||||||
await sendMessageToAllClients({ type: 'import-finished', noteId: firstNote.noteId });
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
sendMessageToAllClients,
|
sendMessageToAllClients,
|
||||||
refreshTree,
|
refreshTree
|
||||||
importNoteCount,
|
|
||||||
importFinished
|
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user