added ImportContext

This commit is contained in:
zadam 2019-02-10 19:36:03 +01:00
parent 5baa251944
commit e4c78f3887
8 changed files with 118 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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