diff --git a/docs/backend_api/Note.html b/docs/backend_api/Note.html
index 068be13d0..5f8d01cc5 100644
--- a/docs/backend_api/Note.html
+++ b/docs/backend_api/Note.html
@@ -270,6 +270,29 @@
+
+
+ utcDateModified |
@@ -372,7 +372,7 @@
Source:
@@ -431,6 +431,320 @@
+ Methods
+
+
+
+
+
+
+
+ (async) getContent() → {Promise.<*>}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+
+
+ -
+ Type
+
+ -
+
+Promise.<*>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ isStringNote() → {boolean}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ true if the note has string content (not binary)
+
+
+
+
+
+ -
+ Type
+
+ -
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (async) setContent() → {Promise}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+
+
+ -
+ Type
+
+ -
+
+Promise
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/backend_api/entities_note.js.html b/docs/backend_api/entities_note.js.html
index b995a7531..06ba0c295 100644
--- a/docs/backend_api/entities_note.js.html
+++ b/docs/backend_api/entities_note.js.html
@@ -42,8 +42,6 @@ const LABEL_DEFINITION = 'label-definition';
const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
-const STRING_MIME_TYPES = ["application/x-javascript"];
-
/**
* This represents a Note which is a central object in the Trilium Notes project.
*
@@ -53,6 +51,7 @@ const STRING_MIME_TYPES = ["application/x-javascript"];
* @property {string} title - note title
* @property {boolean} isProtected - true if note is protected
* @property {boolean} isDeleted - true if note is deleted
+ * @property {boolean} isErased - true if note's content is erased after it has been deleted
* @property {string} dateCreated - local date time (with offset)
* @property {string} dateModified - local date time (with offset)
* @property {string} utcDateCreated
@@ -72,7 +71,7 @@ class Note extends Entity {
super(row);
this.isProtected = !!this.isProtected;
- /* true if content (meaning any kind of potentially encrypted content) is either not encrypted
+ /* true if content is either not encrypted
* or encrypted, but with available protected session (so effectively decrypted) */
this.isContentAvailable = true;
@@ -81,7 +80,7 @@ class Note extends Entity {
this.isContentAvailable = protectedSessionService.isProtectedSessionAvailable();
if (this.isContentAvailable) {
- protectedSessionService.decryptNote(this);
+ this.title = protectedSessionService.decryptString(this.title);
}
else {
this.title = "[protected]";
@@ -116,7 +115,7 @@ class Note extends Entity {
if (this.isProtected) {
if (this.isContentAvailable) {
- protectedSessionService.decryptNoteContent(this);
+ this.content = this.content === null ? null : protectedSessionService.decrypt(this.content);
}
else {
this.content = "";
@@ -142,7 +141,7 @@ class Note extends Entity {
/** @returns {Promise} */
async setContent(content) {
- // force updating note itself so that dateChanged is represented correctly even for the content
+ // force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true;
await this.save();
@@ -157,7 +156,7 @@ class Note extends Entity {
if (this.isProtected) {
if (this.isContentAvailable) {
- protectedSessionService.encryptNoteContent(pojo);
+ pojo.content = protectedSessionService.encrypt(pojo.content);
}
else {
throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`);
@@ -199,9 +198,7 @@ class Note extends Entity {
/** @returns {boolean} true if the note has string content (not binary) */
isStringNote() {
- return ["text", "code", "relation-map", "search"].includes(this.type)
- || this.mime.startsWith('text/')
- || STRING_MIME_TYPES.includes(this.mime);
+ return utils.isStringNote(this.type, this.mime);
}
/** @returns {string} JS script environment - either "frontend" or "backend" */
@@ -447,7 +444,7 @@ class Note extends Entity {
}
/**
- * Creates given attribute name-value pair if it doesn't exist.
+ * Update's given attribute's value or creates it if it doesn't exist
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@@ -456,9 +453,17 @@ class Note extends Entity {
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
- let attr = attributes.find(attr => attr.type === type && (value === undefined || attr.value === value));
+ let attr = attributes.find(attr => attr.type === type && attr.name === name);
- if (!attr) {
+ if (attr) {
+ if (attr.value !== value) {
+ attr.value = value;
+ await attr.save();
+
+ this.invalidateAttributeCache();
+ }
+ }
+ else {
attr = new Attribute({
noteId: this.noteId,
type: type,
@@ -560,7 +565,7 @@ class Note extends Entity {
async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); }
/**
- * Create label name-value pair if it doesn't exist yet.
+ * Update's given label's value or creates it if it doesn't exist
*
* @param {string} name - label name
* @param {string} [value] - label value
@@ -569,7 +574,7 @@ class Note extends Entity {
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
/**
- * Create relation name-value pair if it doesn't exist yet.
+ * Update's given relation's value or creates it if it doesn't exist
*
* @param {string} name - relation name
* @param {string} [value] - relation value (noteId)
@@ -774,7 +779,7 @@ class Note extends Entity {
updatePojo(pojo) {
if (pojo.isProtected) {
if (this.isContentAvailable) {
- protectedSessionService.encryptNote(pojo);
+ pojo.title = protectedSessionService.encrypt(pojo.title);
}
else {
// updating protected note outside of protected session means we will keep original ciphertexts
diff --git a/docs/backend_api/entities_note_revision.js.html b/docs/backend_api/entities_note_revision.js.html
index 112e22a8d..50f8befae 100644
--- a/docs/backend_api/entities_note_revision.js.html
+++ b/docs/backend_api/entities_note_revision.js.html
@@ -31,6 +31,10 @@
const Entity = require('./entity');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
+const utils = require('../services/utils');
+const sql = require('../services/sql');
+const dateUtils = require('../services/date_utils');
+const syncTableService = require('../services/sync_table');
/**
* NoteRevision represents snapshot of note's title and content at some point in the past. It's used for seamless note versioning.
@@ -40,19 +44,19 @@ const repository = require('../services/repository');
* @param {string} type
* @param {string} mime
* @param {string} title
- * @param {string} content
* @param {string} isProtected
- * @param {string} dateModifiedFrom
- * @param {string} dateModifiedTo
- * @param {string} utcDateModifiedFrom
- * @param {string} utcDateModifiedTo
+ * @param {string} dateLastEdited
+ * @param {string} dateCreated
+ * @param {string} utcDateLastEdited
+ * @param {string} utcDateCreated
+ * @param {string} utcDateModified
*
* @extends Entity
*/
class NoteRevision extends Entity {
static get entityName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
- static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo", "utcDateModifiedFrom", "utcDateModifiedTo"]; }
+ static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "contentLength", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified"]; }
constructor(row) {
super(row);
@@ -60,7 +64,12 @@ class NoteRevision extends Entity {
this.isProtected = !!this.isProtected;
if (this.isProtected) {
- protectedSessionService.decryptNoteRevision(this);
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ this.title = protectedSessionService.decryptString(this.title);
+ }
+ else {
+ this.title = "[Protected]";
+ }
}
}
@@ -68,12 +77,98 @@ class NoteRevision extends Entity {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
- beforeSaving() {
- if (this.isProtected) {
- protectedSessionService.encryptNoteRevision(this);
+ /** @returns {boolean} true if the note has string content (not binary) */
+ isStringNote() {
+ return utils.isStringNote(this.type, this.mime);
+ }
+
+ /*
+ * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
+ * part of NoteRevision entity with it's own sync. Reason behind this hybrid design is that
+ * content can be quite large and it's not necessary to load it / fill memory for any note access even
+ * if we don't need a content, especially for bulk operations like search.
+ *
+ * This is the same approach as is used for Note's content.
+ */
+
+ /** @returns {Promise<*>} */
+ async getContent(silentNotFoundError = false) {
+ if (this.content === undefined) {
+ const res = await sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
+
+ if (!res) {
+ if (silentNotFoundError) {
+ return undefined;
+ }
+ else {
+ throw new Error("Cannot find note revision content for noteRevisionId=" + this.noteRevisionId);
+ }
+ }
+
+ this.content = res.content;
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ this.content = protectedSessionService.decrypt(this.content);
+ }
+ else {
+ this.content = "";
+ }
+ }
+
+ if (this.isStringNote()) {
+ this.content = this.content === null
+ ? ""
+ : this.content.toString("UTF-8");
+ }
}
- super.beforeSaving();
+ return this.content;
+ }
+
+ /** @returns {Promise} */
+ async setContent(content) {
+ // force updating note itself so that utcDateModified is represented correctly even for the content
+ this.forcedChange = true;
+ this.contentLength = content.length;
+ await this.save();
+
+ this.content = content;
+
+ const pojo = {
+ noteRevisionId: this.noteRevisionId,
+ content: content,
+ utcDateModified: dateUtils.utcNowDateTime(),
+ hash: utils.hash(this.noteRevisionId + "|" + content)
+ };
+
+ if (this.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ pojo.content = protectedSessionService.encrypt(pojo.content);
+ }
+ else {
+ throw new Error(`Cannot update content of noteRevisionId=${this.noteRevisionId} since we're out of protected session.`);
+ }
+ }
+
+ await sql.upsert("note_revision_contents", "noteRevisionId", pojo);
+
+ await syncTableService.addNoteRevisionContentSync(this.noteRevisionId);
+ }
+
+ // cannot be static!
+ updatePojo(pojo) {
+ if (pojo.isProtected) {
+ if (protectedSessionService.isProtectedSessionAvailable()) {
+ pojo.title = protectedSessionService.encrypt(pojo.title);
+ }
+ else {
+ // updating protected note outside of protected session means we will keep original ciphertexts
+ delete pojo.title;
+ }
+ }
+
+ delete pojo.content;
}
}
diff --git a/docs/frontend_api/NoteShort.html b/docs/frontend_api/NoteShort.html
index d120e54a5..1823f4bfc 100644
--- a/docs/frontend_api/NoteShort.html
+++ b/docs/frontend_api/NoteShort.html
@@ -30,10 +30,7 @@
NoteShort(treeCache, row, branches)
- FIXME: rethink how attributes are cached in Note entities since they are long lived inside the cache.
-Attribute cache should be limited to "transaction".
-
-This note's representation is used in note tree and is kept in TreeCache.
+ This note's representation is used in note tree and is kept in TreeCache.
@@ -191,7 +188,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -281,7 +278,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -349,7 +346,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -417,7 +414,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -475,7 +472,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -533,7 +530,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -591,7 +588,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -649,7 +646,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -707,7 +704,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -775,7 +772,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -843,7 +840,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -901,7 +898,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -959,7 +956,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1107,7 +1104,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1274,7 +1271,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1448,7 +1445,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1554,7 +1551,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1656,7 +1653,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1758,7 +1755,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -1860,7 +1857,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2011,7 +2008,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2178,7 +2175,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2345,7 +2342,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2500,7 +2497,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2606,7 +2603,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2708,7 +2705,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -2859,7 +2856,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3026,7 +3023,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3193,7 +3190,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3348,7 +3345,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3518,7 +3515,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3669,7 +3666,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3779,7 +3776,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -3953,7 +3950,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -4059,7 +4056,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -4210,7 +4207,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -4365,7 +4362,7 @@ This note's representation is used in note tree and is kept in TreeCache.
Source:
@@ -4476,7 +4473,7 @@ Cache is note instance scoped.
Source:
@@ -4560,7 +4557,7 @@ Cache is note instance scoped.
Source:
diff --git a/docs/frontend_api/entities_note_short.js.html b/docs/frontend_api/entities_note_short.js.html
index feac110e3..dd056d723 100644
--- a/docs/frontend_api/entities_note_short.js.html
+++ b/docs/frontend_api/entities_note_short.js.html
@@ -28,7 +28,6 @@
import server from '../services/server.js';
import Attribute from './attribute.js';
-import branches from "../services/branches.js";
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
@@ -36,9 +35,6 @@ const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
- * FIXME: rethink how attributes are cached in Note entities since they are long lived inside the cache.
- * Attribute cache should be limited to "transaction".
- *
* This note's representation is used in note tree and is kept in TreeCache.
*/
class NoteShort {
diff --git a/docs/frontend_api/global.html b/docs/frontend_api/global.html
index 22f49fed0..7f4006e31 100644
--- a/docs/frontend_api/global.html
+++ b/docs/frontend_api/global.html
@@ -204,7 +204,7 @@
- name of the JAM icon to be used (e.g. "clock" for "jam-clock" icon) |
+ name of the boxicon to be used (e.g. "time" for "bx-time" icon) |
|
diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html
index 60cc942ba..82a89d2ac 100644
--- a/docs/frontend_api/services_frontend_script_api.js.html
+++ b/docs/frontend_api/services_frontend_script_api.js.html
@@ -100,7 +100,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
/**
* @typedef {Object} ToolbarButtonOptions
* @property {string} title
- * @property {string} [icon] - name of the JAM icon to be used (e.g. "clock" for "jam-clock" icon)
+ * @property {string} [icon] - name of the boxicon to be used (e.g. "time" for "bx-time" icon)
* @property {function} action - callback handling the click on the button
* @property {string} [shortcut] - keyboard shortcut for the button, e.g. "alt+t"
*/
@@ -118,7 +118,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
.click(opts.action);
if (opts.icon) {
- button.append($("<span>").addClass("jam jam-" + opts.icon))
+ button.append($("<span>").addClass("bx bx-" + opts.icon))
.append(" ");
}
diff --git a/package-lock.json b/package-lock.json
index a02db234f..9c594c099 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2411,9 +2411,9 @@
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI="
},
"dayjs": {
- "version": "1.8.16",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.16.tgz",
- "integrity": "sha512-XPmqzWz/EJiaRHjBqSJ2s6hE/BUoCIHKgdS2QPtTQtKcS9E4/Qn0WomoH1lXanWCzri+g7zPcuNV4aTZ8PMORQ=="
+ "version": "1.8.17",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.17.tgz",
+ "integrity": "sha512-47VY/htqYqr9GHd7HW/h56PpQzRBSJcxIQFwqL3P20bMF/3az5c3PWdVY3LmPXFl6cQCYHL7c79b9ov+2bOBbw=="
},
"debug": {
"version": "4.1.1",
diff --git a/package.json b/package.json
index 57caefc2f..970eb3899 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"commonmark": "0.29.0",
"cookie-parser": "1.4.4",
"csurf": "1.10.0",
- "dayjs": "1.8.16",
+ "dayjs": "1.8.17",
"debug": "4.1.1",
"ejs": "2.7.1",
"electron-debug": "3.0.1",
diff --git a/src/entities/note.js b/src/entities/note.js
index a07fa433d..641b764f1 100644
--- a/src/entities/note.js
+++ b/src/entities/note.js
@@ -416,7 +416,7 @@ class Note extends Entity {
}
/**
- * Creates given attribute name-value pair if it doesn't exist.
+ * Update's given attribute's value or creates it if it doesn't exist
*
* @param {string} type - attribute type (label, relation, etc.)
* @param {string} name - attribute name
@@ -425,9 +425,17 @@ class Note extends Entity {
*/
async setAttribute(type, name, value) {
const attributes = await this.getOwnedAttributes();
- let attr = attributes.find(attr => attr.type === type && (value === undefined || attr.value === value));
+ let attr = attributes.find(attr => attr.type === type && attr.name === name);
- if (!attr) {
+ if (attr) {
+ if (attr.value !== value) {
+ attr.value = value;
+ await attr.save();
+
+ this.invalidateAttributeCache();
+ }
+ }
+ else {
attr = new Attribute({
noteId: this.noteId,
type: type,
@@ -529,7 +537,7 @@ class Note extends Entity {
async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); }
/**
- * Create label name-value pair if it doesn't exist yet.
+ * Update's given label's value or creates it if it doesn't exist
*
* @param {string} name - label name
* @param {string} [value] - label value
@@ -538,7 +546,7 @@ class Note extends Entity {
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); }
/**
- * Create relation name-value pair if it doesn't exist yet.
+ * Update's given relation's value or creates it if it doesn't exist
*
* @param {string} name - relation name
* @param {string} [value] - relation value (noteId)
diff --git a/src/public/javascripts/entities/note_short.js b/src/public/javascripts/entities/note_short.js
index 9d93c2ff5..a29844803 100644
--- a/src/public/javascripts/entities/note_short.js
+++ b/src/public/javascripts/entities/note_short.js
@@ -1,6 +1,5 @@
import server from '../services/server.js';
import Attribute from './attribute.js';
-import branches from "../services/branches.js";
const LABEL = 'label';
const LABEL_DEFINITION = 'label-definition';
diff --git a/src/public/javascripts/services/note_detail_image.js b/src/public/javascripts/services/note_detail_image.js
index c6ac8417f..546e2d5a3 100644
--- a/src/public/javascripts/services/note_detail_image.js
+++ b/src/public/javascripts/services/note_detail_image.js
@@ -1,6 +1,7 @@
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
+import noteDetailService from "./note_detail.js";
class NoteDetailImage {
/**
@@ -12,14 +13,16 @@ class NoteDetailImage {
this.$imageWrapper = ctx.$tabContent.find('.note-detail-image-wrapper');
this.$imageView = ctx.$tabContent.find('.note-detail-image-view');
this.$copyToClipboardButton = ctx.$tabContent.find(".image-copy-to-clipboard");
+ this.$uploadNewRevisionButton = ctx.$tabContent.find(".image-upload-new-revision");
+ this.$uploadNewRevisionInput = ctx.$tabContent.find(".image-upload-new-revision-input");
this.$fileName = ctx.$tabContent.find(".image-filename");
this.$fileType = ctx.$tabContent.find(".image-filetype");
this.$fileSize = ctx.$tabContent.find(".image-filesize");
this.$imageDownloadButton = ctx.$tabContent.find(".image-download");
- this.$imageDownloadButton.click(() => utils.download(this.getFileUrl()));
+ this.$imageDownloadButton.on('click', () => utils.download(this.getFileUrl()));
- this.$copyToClipboardButton.click(() => {
+ this.$copyToClipboardButton.on('click',() => {
this.$imageWrapper.attr('contenteditable','true');
try {
@@ -39,6 +42,34 @@ class NoteDetailImage {
this.$imageWrapper.removeAttr('contenteditable');
}
});
+
+ this.$uploadNewRevisionButton.on("click", () => {
+ this.$uploadNewRevisionInput.trigger("click");
+ });
+
+ this.$uploadNewRevisionInput.on('change', async () => {
+ const formData = new FormData();
+ formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
+
+ const result = await $.ajax({
+ url: baseApiUrl + 'images/' + this.ctx.note.noteId,
+ headers: server.getHeaders(),
+ data: formData,
+ type: 'PUT',
+ timeout: 60 * 60 * 1000,
+ contentType: false, // NEEDED, DON'T REMOVE THIS
+ processData: false, // NEEDED, DON'T REMOVE THIS
+ });
+
+ if (result.uploaded) {
+ toastService.showMessage("New revision of the image has been uploaded.")
+
+ await noteDetailService.reload();
+ }
+ else {
+ toastService.showError("Could not upload new revision of the image: " + result.message);
+ }
+ });
}
async render() {
@@ -51,7 +82,9 @@ class NoteDetailImage {
this.$fileSize.text((attributeMap.fileSize || "?") + " bytes");
this.$fileType.text(this.ctx.note.mime);
- this.$imageView.prop("src", `api/images/${this.ctx.note.noteId}/${this.ctx.note.title}`);
+ const imageHash = this.ctx.note.utcDateModified.replace(" ", "_");
+
+ this.$imageView.prop("src", `api/images/${this.ctx.note.noteId}/${this.ctx.note.title}?${imageHash}`);
}
selectImage(element) {
diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js
index 5be97ea45..9bf806950 100644
--- a/src/routes/api/clipper.js
+++ b/src/routes/api/clipper.js
@@ -82,7 +82,7 @@ async function addImagesToNote(images, note, content) {
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
- const {note: imageNote, url} = await imageService.saveImage(buffer, filename, note.noteId, true);
+ const {note: imageNote, url} = await imageService.saveImage(note.noteId, buffer, filename, true);
await new Attribute({
noteId: note.noteId,
diff --git a/src/routes/api/image.js b/src/routes/api/image.js
index a0f6ba4aa..ed2d0ea43 100644
--- a/src/routes/api/image.js
+++ b/src/routes/api/image.js
@@ -25,8 +25,8 @@ async function returnImage(req, res) {
}
async function uploadImage(req) {
- const noteId = req.query.noteId;
- const file = req.file;
+ const {noteId} = req.query;
+ const {file} = req;
const note = await repository.getNote(noteId);
@@ -38,7 +38,7 @@ async function uploadImage(req) {
return [400, "Unknown image type: " + file.mimetype];
}
- const {url} = await imageService.saveImage(file.buffer, file.originalname, noteId, true);
+ const {url} = await imageService.saveImage(noteId, file.buffer, file.originalname, true);
return {
uploaded: true,
@@ -46,7 +46,30 @@ async function uploadImage(req) {
};
}
+async function updateImage(req) {
+ const {noteId} = req.params;
+ const {file} = req;
+
+ const note = await repository.getNote(noteId);
+
+ if (!note) {
+ return [404, `Note ${noteId} doesn't exist.`];
+ }
+
+ if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.mimetype)) {
+ return {
+ uploaded: false,
+ message: "Unknown image type: " + file.mimetype
+ };
+ }
+
+ await imageService.updateImage(noteId, file.buffer, file.originalname);
+
+ return { uploaded: true };
+}
+
module.exports = {
returnImage,
- uploadImage
+ uploadImage,
+ updateImage
};
\ No newline at end of file
diff --git a/src/routes/api/sender.js b/src/routes/api/sender.js
index 49a2a5462..06d87d7b4 100644
--- a/src/routes/api/sender.js
+++ b/src/routes/api/sender.js
@@ -16,7 +16,7 @@ async function uploadImage(req) {
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
- const {noteId} = await imageService.saveImage(file.buffer, originalName, parentNote.noteId, true);
+ const {noteId} = await imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
return {
noteId: noteId
diff --git a/src/routes/routes.js b/src/routes/routes.js
index f28a3e697..4b4d4e43a 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -171,6 +171,7 @@ function register(app) {
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], imageRoute.uploadImage, apiResultHandler);
+ route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddleware, csrfMiddleware], imageRoute.updateImage, apiResultHandler);
apiRoute(GET, '/api/recent-changes', recentChangesApiRoute.getRecentChanges);
diff --git a/src/services/image.js b/src/services/image.js
index 41fc126f0..9b364b227 100644
--- a/src/services/image.js
+++ b/src/services/image.js
@@ -13,38 +13,62 @@ const jimp = require('jimp');
const imageType = require('image-type');
const sanitizeFilename = require('sanitize-filename');
-async function saveImage(buffer, originalName, parentNoteId, shrinkImageSwitch) {
- const origImageFormat = imageType(buffer);
+async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
+ const origImageFormat = imageType(uploadBuffer);
if (origImageFormat.ext === "webp") {
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
shrinkImageSwitch = false;
}
- const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(buffer, originalName) : buffer;
+ const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer;
const imageFormat = imageType(finalImageBuffer);
- const parentNote = await repository.getNote(parentNoteId);
+ return {
+ buffer: finalImageBuffer,
+ imageFormat
+ };
+}
+
+async function updateImage(noteId, uploadBuffer, originalName) {
+ const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, true);
+
+ const note = await repository.getNote(noteId);
+
+ note.mime = 'image/' + imageFormat.ext.toLowerCase();
+
+ await note.setContent(buffer);
+
+ await note.setLabel('originalFileName', originalName);
+ await note.setLabel('fileSize', buffer.byteLength);
+}
+
+async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch) {
+ const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, shrinkImageSwitch);
const fileName = sanitizeFilename(originalName);
- const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, {
+ const parentNote = await repository.getNote(parentNoteId);
+
+ const {note} = await noteService.createNote(parentNoteId, fileName, buffer, {
target: 'into',
type: 'image',
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
mime: 'image/' + imageFormat.ext.toLowerCase(),
attributes: [
{ type: 'label', name: 'originalFileName', value: originalName },
- { type: 'label', name: 'fileSize', value: finalImageBuffer.byteLength }
+ { type: 'label', name: 'fileSize', value: buffer.byteLength }
]
});
+ const imageHash = note.utcDateModified.replace(" ", "_");
+
return {
fileName,
note,
noteId: note.noteId,
- url: `api/images/${note.noteId}/${fileName}`
+ url: `api/images/${note.noteId}/${fileName}?${imageHash}`
};
}
@@ -107,5 +131,6 @@ async function optimize(buffer) {
}
module.exports = {
- saveImage
+ saveImage,
+ updateImage
};
\ No newline at end of file
diff --git a/src/services/import/enex.js b/src/services/import/enex.js
index 045187ead..2f4fa7614 100644
--- a/src/services/import/enex.js
+++ b/src/services/import/enex.js
@@ -255,7 +255,7 @@ async function importEnex(taskContext, file, parentNote) {
try {
const originalName = "image." + resource.mime.substr(6);
- const {url} = await imageService.saveImage(resource.content, originalName, noteEntity.noteId, taskContext.data.shrinkImages);
+ const {url} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
const imageLink = `