save paste images locally WIP

This commit is contained in:
zadam 2020-03-25 11:28:44 +01:00
parent a856463173
commit 8a92786012
10 changed files with 196 additions and 40 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

19
package-lock.json generated
View File

@ -4714,6 +4714,11 @@
"concat-stream": "^1.4.7" "concat-stream": "^1.4.7"
} }
}, },
"html-comment-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ=="
},
"html-encoding-sniffer": { "html-encoding-sniffer": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@ -5246,6 +5251,14 @@
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
}, },
"is-svg": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.2.1.tgz",
"integrity": "sha512-PHx3ANecKsKNl5y5+Jvt53Y4J7MfMpbNZkv384QNiswMKAWIbvcqbPz+sYbFKJI8Xv3be01GSFniPmoaP+Ai5A==",
"requires": {
"html-comment-regex": "^1.1.2"
}
},
"is-tar": { "is-tar": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz",
@ -9092,9 +9105,9 @@
"optional": true "optional": true
}, },
"sqlite": { "sqlite": {
"version": "3.0.3", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-3.0.3.tgz", "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-3.0.6.tgz",
"integrity": "sha512-DpofdtBibbiOObtdADGZYE6bvnLpjRG4ut/MDTDau2nK40htOLj1E0c55aOkvbnRVqQ0ZPtjj7PJuKKyS0Ypww==", "integrity": "sha512-5SW7HcN+s3TyqpsxOujXhQDCRSCgsxdiU0peT/Y9CT5T0rAsGLwtpXcMyQ7OzOPQ4YUZ5XiGlrwuuQbszr2xtw==",
"requires": { "requires": {
"sql-template-strings": "^2.2.2", "sql-template-strings": "^2.2.2",
"sqlite3": "^4.0.0" "sqlite3": "^4.0.0"

View File

@ -49,6 +49,7 @@
"imagemin-mozjpeg": "8.0.0", "imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "8.0.0", "imagemin-pngquant": "8.0.0",
"ini": "1.3.5", "ini": "1.3.5",
"is-svg": "^4.2.1",
"jimp": "0.9.6", "jimp": "0.9.6",
"mime-types": "2.1.26", "mime-types": "2.1.26",
"multer": "1.4.2", "multer": "1.4.2",

View File

@ -6,6 +6,11 @@ const TPL = `
.note-actions .dropdown-menu { .note-actions .dropdown-menu {
width: 15em; width: 15em;
} }
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
}
</style> </style>
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle"> <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
@ -46,8 +51,18 @@ export default class NoteActionsWidget extends TabAwareWidget {
} }
refreshWithNote(note) { refreshWithNote(note) {
this.$showSourceButton.prop('disabled', !['text', 'relation-map', 'search', 'code'].includes(note.type)); if (['text', 'relation-map', 'search', 'code'].includes(note.type)) {
this.$exportNoteButton.prop('disabled', note.type !== 'text'); this.$showSourceButton.removeAttr('disabled');
} else {
this.$showSourceButton.attr('disabled', 'disabled');
}
if (note.type === 'text') {
this.$exportNoteButton.removeAttr('disabled');
}
else {
this.$exportNoteButton.attr('disabled', 'disabled');
}
} }
triggerEvent(e, eventName) { triggerEvent(e, eventName) {

View File

@ -18,7 +18,7 @@ async function returnImage(req, res) {
res.set('Content-Type', 'image/png'); res.set('Content-Type', 'image/png');
return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png')); return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
} }
image.mime = image.mime.replace("image/svg", "image/svg+xml");
res.set('Content-Type', image.mime); res.set('Content-Type', image.mime);
res.send(await image.getContent()); res.send(await image.getContent());

View File

@ -13,18 +13,19 @@ const jimp = require('jimp');
const imageType = require('image-type'); const imageType = require('image-type');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
const noteRevisionService = require('./note_revisions.js'); const noteRevisionService = require('./note_revisions.js');
const isSvg = require('is-svg');
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) { async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
const origImageFormat = imageType(uploadBuffer); const origImageFormat = getImageType(uploadBuffer);
if (origImageFormat.ext === "webp") { if (origImageFormat && ["webp", "svg"].includes(origImageFormat.ext)) {
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144 // JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
shrinkImageSwitch = false; shrinkImageSwitch = false;
} }
const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer; const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer;
const imageFormat = imageType(finalImageBuffer); const imageFormat = getImageType(finalImageBuffer);
return { return {
buffer: finalImageBuffer, buffer: finalImageBuffer,
@ -32,6 +33,17 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
}; };
} }
function getImageType(buffer) {
if (isSvg(buffer)) {
return {
ext: 'svg'
}
}
else {
return imageType(buffer);
}
}
async function updateImage(noteId, uploadBuffer, originalName) { async function updateImage(noteId, uploadBuffer, originalName) {
const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, true); const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, true);

View File

@ -14,6 +14,10 @@ const hoistedNoteService = require('../services/hoisted_note');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const log = require('../services/log'); const log = require('../services/log');
const noteRevisionService = require('../services/note_revisions'); const noteRevisionService = require('../services/note_revisions');
const attributeService = require('../services/attributes');
const request = require('./request');
const path = require('path');
const url = require('url');
async function getNewNotePosition(parentNoteId) { async function getNewNotePosition(parentNoteId) {
const maxNotePos = await sql.getValue(` const maxNotePos = await sql.getValue(`
@ -161,31 +165,6 @@ async function createNewNoteWithTarget(target, targetBranchId, params) {
} }
} }
// methods below should be probably just backend API methods
async function createJsonNote(parentNoteId, title, content = {}, params = {}) {
params.parentNoteId = parentNoteId;
params.title = title;
params.type = "code";
params.mime = "application/json";
params.content = JSON.stringify(content, null, '\t');
return await createNewNote(params);
}
async function createTextNote(parentNoteId, title, content = "", params = {}) {
params.parentNoteId = parentNoteId;
params.title = title;
params.type = "text";
params.mime = "text/html";
params.content = content;
return await createNewNote(params);
}
async function protectNoteRecursively(note, protect, includingSubTree, taskContext) { async function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
await protectNote(note, protect); await protectNote(note, protect);
@ -283,6 +262,90 @@ function findRelationMapLinks(content, foundLinks) {
} }
} }
const imageUrlToNoteIdMapping = {};
async function downloadImage(noteId, imageUrl) {
const imageBuffer = await request.getImage(imageUrl);
const parsedUrl = url.parse(imageUrl);
const title = path.basename(parsedUrl.pathname);
const imageService = require('../services/image');
const {note} = await imageService.saveImage(noteId, imageBuffer, title, true);
await note.addLabel('imageUrl', imageUrl);
imageUrlToNoteIdMapping[imageUrl] = note.noteId;
}
const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) {
return content.replace(new RegExp(url, "g"), `api/images/${imageNote.noteId}/${imageNote.title}`);
}
async function downloadImages(noteId, content) {
const re = /<img\s.*?src=['"]([^'">]+)['"]/ig;
let match;
while (match = re.exec(content)) {
const url = match[1];
if (!url.startsWith('api/images/')) {
if (url in downloadImagePromises) {
continue;
}
if (url in imageUrlToNoteIdMapping) {
const imageNote = await repository.getNote(imageUrlToNoteIdMapping[url]);
if (imageNote || imageNote.isDeleted) {
delete imageUrlToNoteIdMapping[url];
}
else {
content = replaceUrl(content, url, imageNote);
continue;
}
}
const existingImage = (await attributeService.getNotesWithLabel('imageUrl', url))
.find(note => note.type === 'image');
if (existingImage) {
imageUrlToNoteIdMapping[url] = existingImage.noteId;
content = replaceUrl(content, url, existingImage);
continue;
}
downloadImagePromises[url] = downloadImage(noteId, url);
}
}
await Promise.all(Object.values(downloadImagePromises)).then(() => {
setTimeout(async () => {
const imageNotes = await repository.getNotes(Object.values(imageUrlToNoteIdMapping));
const origNote = await repository.getNote(noteId);
const origContent = await origNote.getContent();
let updatedContent = origContent;
for (const url in imageUrlToNoteIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToNoteIdMapping[url]);
if (imageNote) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
}
}
if (updatedContent !== origContent) {
await origNote.setContent(updatedContent);
}
}, 5000);
});
}
async function saveLinks(note, content) { async function saveLinks(note, content) {
if (note.type !== 'text' && note.type !== 'relation-map') { if (note.type !== 'text' && note.type !== 'relation-map') {
return content; return content;
@ -299,6 +362,8 @@ async function saveLinks(note, content) {
content = findInternalLinks(content, foundLinks); content = findInternalLinks(content, foundLinks);
content = findExternalLinks(content, foundLinks); content = findExternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks); content = findIncludeNoteLinks(content, foundLinks);
downloadImages(note.noteId, content);
} }
else if (note.type === 'relation-map') { else if (note.type === 'relation-map') {
findRelationMapLinks(content, foundLinks); findRelationMapLinks(content, foundLinks);

View File

@ -3,6 +3,7 @@
const utils = require('./utils'); const utils = require('./utils');
const log = require('./log'); const log = require('./log');
const url = require('url'); const url = require('url');
const syncOptions = require('./sync_options');
// this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs // this service provides abstraction over node's HTTP/HTTPS and electron net.client APIs
// this allows to support system proxy // this allows to support system proxy
@ -78,12 +79,60 @@ function exec(opts) {
catch (e) { catch (e) {
reject(generateError(opts, e.message)); reject(generateError(opts, e.message));
} }
}) });
}
async function getImage(imageUrl) {
const opts = {
method: 'GET',
url: imageUrl,
proxy: await syncOptions.getSyncProxy()
};
const client = getClient(opts);
const proxyAgent = getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url);
return await new Promise(async (resolve, reject) => {
try {
const request = client.request({
method: opts.method,
// url is used by electron net module
url: opts.url,
// 4 fields below are used by http and https node modules
protocol: parsedTargetUrl.protocol,
host: parsedTargetUrl.hostname,
port: parsedTargetUrl.port,
path: parsedTargetUrl.path,
timeout: opts.timeout,
headers: {},
agent: proxyAgent
});
request.on('error', err => reject(generateError(opts, err)));
request.on('response', response => {
if (![200, 201, 204].includes(response.statusCode)) {
reject(generateError(opts, response.statusCode + ' ' + response.statusMessage));
}
const chunks = []
response.on('data', chunk => chunks.push(chunk));
response.on('end', () => resolve(Buffer.concat(chunks)));
});
request.end(undefined);
}
catch (e) {
reject(generateError(opts, e.message));
}
});
} }
function getProxyAgent(opts) { function getProxyAgent(opts) {
if (!opts.proxy) { if (!opts.proxy) {
return; return null;
} }
const {protocol} = url.parse(opts.url); const {protocol} = url.parse(opts.url);
@ -122,5 +171,6 @@ function generateError(opts, message) {
} }
module.exports = { module.exports = {
exec exec,
getImage
}; };

View File

@ -158,7 +158,7 @@ function getContentDisposition(filename) {
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`; return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
} }
const STRING_MIME_TYPES = ["application/x-javascript"]; const STRING_MIME_TYPES = ["application/x-javascript", "image/svg"];
function isStringNote(type, mime) { function isStringNote(type, mime) {
return ["text", "code", "relation-map", "search"].includes(type) return ["text", "code", "relation-map", "search"].includes(type)