@@ -148,6 +149,10 @@ export default class AttachmentDetailWidget extends BasicWidget {
}
}
+ copyAttachmentReferenceToClipboard() {
+ imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content'));
+ }
+
async entitiesReloadedEvent({loadResults}) {
const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId);
diff --git a/src/public/app/widgets/buttons/attachments_actions.js b/src/public/app/widgets/buttons/attachments_actions.js
index 497bd9117..745fb8d1d 100644
--- a/src/public/app/widgets/buttons/attachments_actions.js
+++ b/src/public/app/widgets/buttons/attachments_actions.js
@@ -4,6 +4,7 @@ import dialogService from "../../services/dialog.js";
import toastService from "../../services/toast.js";
import ws from "../../services/ws.js";
import appContext from "../../components/app_context.js";
+import openService from "../../services/open.js";
const TPL = `
@@ -28,8 +29,15 @@ const TPL = `
aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded">
`;
@@ -40,9 +48,30 @@ export default class AttachmentActionsWidget extends BasicWidget {
this.attachment = attachment;
}
+ get attachmentId() {
+ return this.attachment.attachmentId;
+ }
+
doRender() {
this.$widget = $(TPL);
this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
+ this.$widget.find("[data-trigger-command='copyAttachmentReferenceToClipboard']").toggle(this.attachment.role === 'image');
+ }
+
+ async openAttachmentCommand() {
+ await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
+ }
+
+ async downloadAttachmentCommand() {
+ await openService.downloadAttachment(this.attachmentId);
+ }
+
+ async copyAttachmentReferenceToClipboardCommand() {
+ this.parent.copyAttachmentReferenceToClipboard();
+ }
+
+ async openAttachmentExternallyCommand() {
+ await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
}
async deleteAttachmentCommand() {
@@ -50,7 +79,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
return;
}
- await server.remove(`attachments/${this.attachment.attachmentId}`);
+ await server.remove(`attachments/${this.attachmentId}`);
toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
}
@@ -59,7 +88,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
return;
}
- const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`)
+ const {note: newNote} = await server.post(`attachments/${this.attachmentId}/convert-to-note`)
toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`);
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId);
diff --git a/src/public/app/widgets/buttons/note_actions.js b/src/public/app/widgets/buttons/note_actions.js
index b11f8d8a4..58eec0007 100644
--- a/src/public/app/widgets/buttons/note_actions.js
+++ b/src/public/app/widgets/buttons/note_actions.js
@@ -35,7 +35,11 @@ const TPL = `
diff --git a/src/public/app/widgets/note_update_status.js b/src/public/app/widgets/note_update_status.js
deleted file mode 100644
index b73febf0b..000000000
--- a/src/public/app/widgets/note_update_status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import NoteContextAwareWidget from "./note_context_aware_widget.js";
-import server from "../services/server.js";
-import fileWatcher from "../services/file_watcher.js";
-
-const TPL = `
-
`;
-
-export default class NoteUpdateStatusWidget extends NoteContextAwareWidget {
- isEnabled() {
- return super.isEnabled()
- && !!fileWatcher.getFileModificationStatus(this.noteId);
- }
-
- doRender() {
- this.$widget = $(TPL);
-
- this.$filePath = this.$widget.find(".file-path");
- this.$fileLastModified = this.$widget.find(".file-last-modified");
- this.$fileUploadButton = this.$widget.find(".file-upload-button");
-
- this.$fileUploadButton.on("click", async () => {
- await server.post(`notes/${this.noteId}/upload-modified-file`, {
- filePath: this.$filePath.text()
- });
-
- fileWatcher.fileModificationUploaded(this.noteId);
- this.refresh();
- });
-
- this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
- this.$ignoreThisChangeButton.on('click', () => {
- fileWatcher.ignoreModification(this.noteId);
- this.refresh();
- });
- }
-
- refreshWithNote(note) {
- const status = fileWatcher.getFileModificationStatus(note.noteId);
-
- this.$filePath.text(status.filePath);
- this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
- }
-
- openedFileUpdatedEvent(data) {
- if (data.noteId === this.noteId) {
- this.refresh();
- }
- }
-}
diff --git a/src/public/app/widgets/ribbon_widgets/image_properties.js b/src/public/app/widgets/ribbon_widgets/image_properties.js
index 9cf31bf65..9032d0d39 100644
--- a/src/public/app/widgets/ribbon_widgets/image_properties.js
+++ b/src/public/app/widgets/ribbon_widgets/image_properties.js
@@ -69,7 +69,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
this.$fileSize = this.$widget.find(".image-filesize");
this.$openButton = this.$widget.find(".image-open");
- this.$openButton.on('click', () => openService.openNoteExternally(this.noteId, this.note.mime ));
+ this.$openButton.on('click', () => openService.openNoteExternally(this.noteId, this.note.mime));
this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));
diff --git a/src/public/app/widgets/type_widgets/image.js b/src/public/app/widgets/type_widgets/image.js
index 467f9b1cd..d35f9b634 100644
--- a/src/public/app/widgets/type_widgets/image.js
+++ b/src/public/app/widgets/type_widgets/image.js
@@ -1,8 +1,8 @@
import utils from "../../services/utils.js";
-import toastService from "../../services/toast.js";
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import contextMenu from "../../menus/context_menu.js";
+import imageService from "../../services/image.js";
const TPL = `
@@ -73,7 +73,7 @@ class ImageTypeWidget extends TypeWidget {
],
selectMenuItemHandler: ({command}) => {
if (command === 'copyImageReferenceToClipboard') {
- this.copyImageReferenceToClipboard();
+ imageService.copyImageReferenceToClipboard(this.$imageWrapper);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
@@ -98,36 +98,7 @@ class ImageTypeWidget extends TypeWidget {
return;
}
- this.copyImageReferenceToClipboard();
- }
-
- copyImageReferenceToClipboard() {
- this.$imageWrapper.attr('contenteditable','true');
-
- try {
- this.selectImage(this.$imageWrapper.get(0));
-
- const success = document.execCommand('copy');
-
- if (success) {
- toastService.showMessage("Image copied to the clipboard");
- }
- else {
- toastService.showAndLogError("Could not copy the image to clipboard.");
- }
- }
- finally {
- window.getSelection().removeAllRanges();
- this.$imageWrapper.removeAttr('contenteditable');
- }
- }
-
- selectImage(element) {
- const selection = window.getSelection();
- const range = document.createRange();
- range.selectNodeContents(element);
- selection.removeAllRanges();
- selection.addRange(range);
+ imageService.copyImageReferenceToClipboard(this.$imageWrapper);
}
}
diff --git a/src/public/app/widgets/watched_file_update_status.js b/src/public/app/widgets/watched_file_update_status.js
new file mode 100644
index 000000000..f8aa8529c
--- /dev/null
+++ b/src/public/app/widgets/watched_file_update_status.js
@@ -0,0 +1,96 @@
+import NoteContextAwareWidget from "./note_context_aware_widget.js";
+import server from "../services/server.js";
+import fileWatcher from "../services/file_watcher.js";
+
+const TPL = `
+
`;
+
+export default class WatchedFileUpdateStatusWidget extends NoteContextAwareWidget {
+ isEnabled() {
+ const { entityType, entityId } = this.getEntity();
+
+ console.log(entityType, entityId);
+
+ return super.isEnabled() && !!fileWatcher.getFileModificationStatus(entityType, entityId);
+ }
+
+ doRender() {
+ this.$widget = $(TPL);
+
+ this.$filePath = this.$widget.find(".file-path");
+ this.$fileLastModified = this.$widget.find(".file-last-modified");
+ this.$fileUploadButton = this.$widget.find(".file-upload-button");
+
+ this.$fileUploadButton.on("click", async () => {
+ const { entityType, entityId } = this.getEntity();
+
+ await server.post(`${entityType}/${entityId}/upload-modified-file`, {
+ filePath: this.$filePath.text()
+ });
+
+ fileWatcher.fileModificationUploaded(entityType, entityId);
+ this.refresh();
+ });
+
+ this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
+ this.$ignoreThisChangeButton.on('click', () => {
+ const { entityType, entityId } = this.getEntity();
+
+ fileWatcher.ignoreModification(entityType, entityId);
+ this.refresh();
+ });
+ }
+
+ refreshWithNote(note) {
+ const { entityType, entityId } = this.getEntity();
+ const status = fileWatcher.getFileModificationStatus(entityType, entityId);
+
+ console.log("status", status);
+
+ this.$filePath.text(status.filePath);
+ this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
+ }
+
+ getEntity() {
+ if (!this.noteContext) {
+ return {};
+ }
+
+ const { viewScope } = this.noteContext;
+
+ if (viewScope.viewMode === 'attachments' && viewScope.attachmentId) {
+ return {
+ entityType: 'attachments',
+ entityId: viewScope.attachmentId
+ };
+ } else {
+ return {
+ entityType: 'notes',
+ entityId: this.noteId
+ };
+ }
+ }
+
+ openedFileUpdatedEvent(data) {console.log(data);
+ const { entityType, entityId } = this.getEntity();
+
+ if (data.entityType === entityType && data.entityId === entityId) {
+ this.refresh();
+ }
+ }
+}
diff --git a/src/routes/api/files.js b/src/routes/api/files.js
index 7d4911916..90faba559 100644
--- a/src/routes/api/files.js
+++ b/src/routes/api/files.js
@@ -11,6 +11,7 @@ const chokidar = require('chokidar');
const ws = require('../../services/ws');
const becca = require("../../becca/becca");
const NotFoundError = require("../../errors/not_found_error");
+const ValidationError = require("../../errors/validation_error.js");
function updateFile(req) {
const {noteId} = req.params;
@@ -38,61 +39,84 @@ function updateFile(req) {
};
}
-function getFilename(note) {
- // (one) reason we're not using the originFileName (available as label) is that it's not
- // available for older note revisions and thus would be inconsistent
- return utils.formatDownloadTitle(note.title, note.type, note.mime);
+/**
+ * @param {BNote|BAttachment} noteOrAttachment
+ * @param res
+ * @param {boolean} contentDisposition
+ */
+function downloadData(noteOrAttachment, res, contentDisposition) {
+ if (noteOrAttachment.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
+ return res.status(401).send("Protected session not available");
+ }
+
+ if (contentDisposition) {
+ const fileName = noteOrAttachment.getFileName();
+
+ res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
+ }
+
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ res.setHeader('Content-Type', noteOrAttachment.mime);
+
+ res.send(noteOrAttachment.getContent());
}
-function downloadNoteFile(noteId, res, contentDisposition = true) {
+function downloadNoteInt(noteId, res, contentDisposition = true) {
const note = becca.getNote(noteId);
if (!note) {
return res.setHeader("Content-Type", "text/plain")
.status(404)
- .send(`Note ${noteId} doesn't exist.`);
+ .send(`Note '${noteId}' doesn't exist.`);
}
- if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
- return res.status(401).send("Protected session not available");
+ return downloadData(note, res, contentDisposition);
+}
+
+function downloadAttachmentInt(attachmentId, res, contentDisposition = true) {
+ const attachment = becca.getAttachment(attachmentId);
+
+ if (!attachment) {
+ return res.setHeader("Content-Type", "text/plain")
+ .status(404)
+ .send(`Attachment '${attachmentId}' doesn't exist.`);
}
- if (contentDisposition) {
- const filename = getFilename(note);
-
- res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
- }
-
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
- res.setHeader('Content-Type', note.mime);
-
- res.send(note.getContent());
+ return downloadData(attachment, res, contentDisposition);
}
-function downloadFile(req, res) {
- const noteId = req.params.noteId;
+const downloadFile = (req, res) => downloadNoteInt(req.params.noteId, res, true);
+const openFile = (req, res) => downloadNoteInt(req.params.noteId, res, false);
- return downloadNoteFile(noteId, res);
-}
-
-function openFile(req, res) {
- const noteId = req.params.noteId;
-
- return downloadNoteFile(noteId, res, false);
-}
+const downloadAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, true);
+const openAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, false);
function fileContentProvider(req) {
// Read file name from route params.
const note = becca.getNote(req.params.noteId);
- const fileName = getFilename(note);
- let content = note.getContent();
+ if (!note) {
+ throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
+ }
+ return streamContent(note.getContent(), note.getFileName(), note.mime);
+}
+
+function attachmentContentProvider(req) {
+ // Read file name from route params.
+ const attachment = becca.getAttachment(req.params.attachmentId);
+ if (!attachment) {
+ throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
+ }
+
+ return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
+}
+
+function streamContent(content, fileName, mimeType) {
if (typeof content === "string") {
- content = Buffer.from(content, 'utf8');
+ content = Buffer.from(content, 'utf8');
}
const totalSize = content.byteLength;
- const mimeType = note.mime;
const getStream = range => {
if (!range) {
@@ -100,7 +124,7 @@ function fileContentProvider(req) {
return Readable.from(content);
}
// Partial content request.
- const { start, end } = range;
+ const {start, end} = range;
return Readable.from(content.slice(start, end + 1));
}
@@ -113,27 +137,44 @@ function fileContentProvider(req) {
};
}
-function saveToTmpDir(req) {
- const noteId = req.params.noteId;
-
- const note = becca.getNote(noteId);
-
+function saveNoteToTmpDir(req) {
+ const note = becca.getNote(req.params.noteId);
if (!note) {
- throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
+ throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
}
- const tmpObj = tmp.fileSync({postfix: getFilename(note)});
+ const fileName = note.getFileName();
+ const content = note.getContent();
- fs.writeSync(tmpObj.fd, note.getContent());
+ return saveToTmpDir(fileName, content, 'notes', note.noteId);
+}
+
+function saveAttachmentToTmpDir(req) {
+ const attachment = becca.getAttachment(req.params.attachmentId);
+ if (!attachment) {
+ throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
+ }
+
+ const fileName = attachment.getFileName();
+ const content = attachment.getContent();
+
+ return saveToTmpDir(fileName, content, 'attachments', attachment.attachmentId);
+}
+
+function saveToTmpDir(fileName, content, entityType, entityId) {
+ const tmpObj = tmp.fileSync({ postfix: fileName });
+
+ fs.writeSync(tmpObj.fd, content);
fs.closeSync(tmpObj.fd);
- log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`);
+ log.info(`Saved temporary file ${tmpObj.name}`);
if (utils.isElectron()) {
chokidar.watch(tmpObj.name).on('change', (path, stats) => {
ws.sendMessageToAllClients({
type: 'openedFileUpdated',
- noteId: noteId,
+ entityType: entityType,
+ entityId: entityId,
lastModifiedMs: stats.atimeMs,
filePath: tmpObj.name
});
@@ -145,11 +186,63 @@ function saveToTmpDir(req) {
};
}
+function uploadModifiedFileToNote(req) {
+ const noteId = req.params.noteId;
+ const {filePath} = req.body;
+
+ const note = becca.getNote(noteId);
+
+ if (!note) {
+ throw new NotFoundError(`Note '${noteId}' has not been found`);
+ }
+
+ log.info(`Updating note '${noteId}' with content from '${filePath}'`);
+
+ note.saveNoteRevision();
+
+ const fileContent = fs.readFileSync(filePath);
+
+ if (!fileContent) {
+ throw new ValidationError(`File '${fileContent}' is empty`);
+ }
+
+ note.setContent(fileContent);
+}
+
+function uploadModifiedFileToAttachment(req) {
+ const {attachmentId} = req.params;
+ const {filePath} = req.body;
+
+ const attachment = becca.getAttachment(attachmentId);
+
+ if (!attachment) {
+ throw new NotFoundError(`Attachment '${attachmentId}' has not been found`);
+ }
+
+ log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);
+
+ attachment.getNote().saveNoteRevision();
+
+ const fileContent = fs.readFileSync(filePath);
+
+ if (!fileContent) {
+ throw new ValidationError(`File '${fileContent}' is empty`);
+ }
+
+ attachment.setContent(fileContent);
+}
+
module.exports = {
updateFile,
openFile,
fileContentProvider,
downloadFile,
- downloadNoteFile,
- saveToTmpDir
+ downloadNoteInt,
+ saveNoteToTmpDir,
+ openAttachment,
+ downloadAttachment,
+ saveAttachmentToTmpDir,
+ attachmentContentProvider,
+ uploadModifiedFileToNote,
+ uploadModifiedFileToAttachment
};
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index 9663d0536..97e81411d 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -229,29 +229,6 @@ function getDeleteNotesPreview(req) {
};
}
-function uploadModifiedFile(req) {
- const noteId = req.params.noteId;
- const {filePath} = req.body;
-
- const note = becca.getNote(noteId);
-
- if (!note) {
- throw new NotFoundError(`Note '${noteId}' has not been found`);
- }
-
- log.info(`Updating note '${noteId}' with content from ${filePath}`);
-
- note.saveNoteRevision();
-
- const fileContent = fs.readFileSync(filePath);
-
- if (!fileContent) {
- throw new ValidationError(`File '${fileContent}' is empty`);
- }
-
- note.setContent(fileContent);
-}
-
function forceSaveNoteRevision(req) {
const {noteId} = req.params;
const note = becca.getNote(noteId);
@@ -294,7 +271,6 @@ module.exports = {
eraseDeletedNotesNow,
eraseUnusedAttachmentsNow,
getDeleteNotesPreview,
- uploadModifiedFile,
forceSaveNoteRevision,
convertNoteToAttachment
};
diff --git a/src/routes/custom.js b/src/routes/custom.js
index f32f7445f..c99db4066 100644
--- a/src/routes/custom.js
+++ b/src/routes/custom.js
@@ -1,5 +1,5 @@
const log = require('../services/log');
-const fileUploadService = require('./api/files');
+const fileService = require('./api/files');
const scriptService = require('../services/script');
const cls = require('../services/cls');
const sql = require("../services/sql");
@@ -26,7 +26,7 @@ function handleRequest(req, res) {
match = path.match(regex);
}
catch (e) {
- log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ${e.stack}`);
+ log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${e.message}, stack: ${e.stack}`);
continue;
}
@@ -37,7 +37,7 @@ function handleRequest(req, res) {
if (attr.name === 'customRequestHandler') {
const note = attr.getNote();
- log.info(`Handling custom request "${path}" with note ${note.noteId}`);
+ log.info(`Handling custom request '${path}' with note '${note.noteId}'`);
try {
scriptService.executeNote(note, {
@@ -47,7 +47,7 @@ function handleRequest(req, res) {
});
}
catch (e) {
- log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
+ log.error(`Custom handler '${note.noteId}' failed with: ${e.message}, ${e.stack}`);
res.setHeader("Content-Type", "text/plain")
.status(500)
@@ -55,16 +55,16 @@ function handleRequest(req, res) {
}
}
else if (attr.name === 'customResourceProvider') {
- fileUploadService.downloadNoteFile(attr.noteId, res);
+ fileService.downloadNoteInt(attr.noteId, res);
}
else {
- throw new Error(`Unrecognized attribute name ${attr.name}`);
+ throw new Error(`Unrecognized attribute name '${attr.name}'`);
}
return; // only first handler is executed
}
- const message = `No handler matched for custom ${path} request.`;
+ const message = `No handler matched for custom '${path}' request.`;
log.info(message);
res.setHeader("Content-Type", "text/plain")
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 8e8dcc61d..ff9ab57be 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -122,7 +122,6 @@ function register(app) {
apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle);
apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
- apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToParentNote);
@@ -137,7 +136,8 @@ function register(app) {
route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
// this "hacky" path is used for easier referencing of CSS resources
route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
- apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir);
+ apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveNoteToTmpDir);
+ apiRoute(PST, '/api/notes/:noteId/upload-modified-file', filesRoute.uploadModifiedFileToNote);
apiRoute(PST, '/api/notes/:noteId/convert-to-attachment', notesApiRoute.convertNoteToAttachment);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
@@ -154,6 +154,16 @@ function register(app) {
apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote);
apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment);
route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
+ route(GET, '/api/attachments/:attachmentId/open', [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
+ route(GET, '/api/attachments/:attachmentId/open-partial', [auth.checkApiAuthOrElectron],
+ createPartialContentHandler(filesRoute.attachmentContentProvider, {
+ debug: (string, extra) => { console.log(string, extra); }
+ }));
+ route(GET, '/api/attachments/:attachmentId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
+ // this "hacky" path is used for easier referencing of CSS resources
+ route(GET, '/api/attachments/download/:attachmentId', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
+ apiRoute(PST, '/api/attachments/:attachmentId/save-to-tmp-dir', filesRoute.saveAttachmentToTmpDir);
+ apiRoute(PST, '/api/attachments/:attachmentId/upload-modified-file', filesRoute.uploadModifiedFileToAttachment);
apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
diff --git a/src/services/notes.js b/src/services/notes.js
index 796c89070..091d2f6e9 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -362,8 +362,9 @@ function checkImageAttachments(note, content) {
const existingAttachmentIds = new Set(imageAttachments.map(att => att.attachmentId));
const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
+ const unknownAttachments = becca.getAttachments(unknownAttachmentIds);
- for (const unknownAttachment of becca.getAttachments(unknownAttachmentIds)) {
+ for (const unknownAttachment of unknownAttachments) {
// the attachment belongs to a different note (was copy pasted), we need to make a copy for this note.
const newAttachment = unknownAttachment.copy();
newAttachment.parentId = note.noteId;
@@ -374,7 +375,10 @@ function checkImageAttachments(note, content) {
log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`);
}
- return content;
+ return {
+ forceFrontendReload: unknownAttachments.length > 0,
+ content
+ };
}
@@ -591,6 +595,7 @@ function saveLinks(note, content) {
}
const foundLinks = [];
+ let forceFrontendReload = false;
if (note.type === 'text') {
content = downloadImages(note.noteId, content);
@@ -599,7 +604,7 @@ function saveLinks(note, content) {
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
- content = checkImageAttachments(note, content);
+ ({forceFrontendReload, content} = checkImageAttachments(note, content));
}
else if (note.type === 'relationMap') {
findRelationMapLinks(content, foundLinks);
@@ -643,7 +648,7 @@ function saveLinks(note, content) {
unusedLink.markAsDeleted();
}
- return content;
+ return { forceFrontendReload, content };
}
/** @param {BNote} note */
@@ -677,9 +682,9 @@ function updateNoteData(noteId, content) {
saveNoteRevisionIfNeeded(note);
- content = saveLinks(note, content);
+ const { forceFrontendReload, content: newContent } = saveLinks(note, content);
- note.setContent(content);
+ note.setContent(newContent, { forceFrontendReload });
}
/**
@@ -780,15 +785,15 @@ function scanForLinks(note, content) {
try {
sql.transactional(() => {
- const newContent = saveLinks(note, content);
+ const { forceFrontendReload, content: newContent } = saveLinks(note, content);
if (content !== newContent) {
- note.setContent(newContent);
+ note.setContent(newContent, { forceFrontendReload });
}
});
}
catch (e) {
- log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);
+ log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`);
}
}
diff --git a/src/services/utils.js b/src/services/utils.js
index 5a9cdb7a5..77a48e379 100644
--- a/src/services/utils.js
+++ b/src/services/utils.js
@@ -199,33 +199,33 @@ function replaceAll(string, replaceWhat, replaceWith) {
return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
}
-function formatDownloadTitle(filename, type, mime) {
- if (!filename) {
- filename = "untitled";
+function formatDownloadTitle(fileName, type, mime) {
+ if (!fileName) {
+ fileName = "untitled";
}
- filename = sanitize(filename);
+ fileName = sanitize(fileName);
if (type === 'text') {
- return `${filename}.html`;
+ return `${fileName}.html`;
} else if (['relationMap', 'canvas', 'search'].includes(type)) {
- return `${filename}.json`;
+ return `${fileName}.json`;
} else {
if (!mime) {
- return filename;
+ return fileName;
}
mime = mime.toLowerCase();
- const filenameLc = filename.toLowerCase();
+ const filenameLc = fileName.toLowerCase();
const extensions = mimeTypes.extensions[mime];
if (!extensions || extensions.length === 0) {
- return filename;
+ return fileName;
}
for (const ext of extensions) {
if (filenameLc.endsWith(`.${ext}`)) {
- return filename;
+ return fileName;
}
}
@@ -234,10 +234,10 @@ function formatDownloadTitle(filename, type, mime) {
// the current name without fake extension. It's possible that the title still preserves to correct
// extension too
- return filename;
+ return fileName;
}
- return `${filename}.${extensions[0]}`;
+ return `${fileName}.${extensions[0]}`;
}
}