From 330e7ac08e67c4a5461a6cb85a0e6dfb6ad9a116 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 25 Apr 2023 00:01:58 +0200 Subject: [PATCH] copying attachments WIP --- src/becca/becca.js | 8 ++++++++ src/becca/entities/abstract_becca_entity.js | 20 ++++++++++++++++--- src/becca/entities/battachment.js | 3 +-- src/becca/entities/bnote.js | 4 ++++ .../type_widgets/abstract_text_type_widget.js | 2 -- .../other/attachment_erasure_timeout.js | 4 ++-- src/services/notes.js | 20 +++++++++++++++++-- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/becca/becca.js b/src/becca/becca.js index 1b3b72d4f..4e63dc7e5 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -2,6 +2,7 @@ const sql = require("../services/sql"); const NoteSet = require("../services/search/note_set"); +const BAttachment = require("./entities/battachment.js"); /** * Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca. @@ -129,6 +130,13 @@ class Becca { return row ? new BAttachment(row) : null; } + /** @returns {BAttachment[]} */ + getAttachments(attachmentIds) { + const BAttachment = require("./entities/battachment"); // avoiding circular dependency problems + return sql.getManyRows("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds) + .map(row => new BAttachment(row)); + } + /** @returns {BOption|null} */ getOption(name) { return this.options[name]; diff --git a/src/becca/entities/abstract_becca_entity.js b/src/becca/entities/abstract_becca_entity.js index 2934c7856..072455b50 100644 --- a/src/becca/entities/abstract_becca_entity.js +++ b/src/becca/entities/abstract_becca_entity.js @@ -148,9 +148,17 @@ class AbstractBeccaEntity { content = Buffer.isBuffer(content) ? content : Buffer.from(content); } + let unencryptedContentForHashCalculation = content; + if (this.isProtected) { if (protectedSessionService.isProtectedSessionAvailable()) { content = protectedSessionService.encrypt(content); + + // this is to make sure that the calculated hash/blobId is different for an encrypted note and decrypted + const encryptedPrefixSuffix = "ThisIsEncryptedContent&^$#$1%&8*)(^%$5#@"; + unencryptedContentForHashCalculation = Buffer.isBuffer(unencryptedContentForHashCalculation) + ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContentForHashCalculation, Buffer.from(encryptedPrefixSuffix)]) + : `${encryptedPrefixSuffix}${unencryptedContentForHashCalculation}${encryptedPrefixSuffix}`; } else { throw new Error(`Cannot update content of blob since we're out of protected session.`); @@ -158,7 +166,7 @@ class AbstractBeccaEntity { } sql.transactional(() => { - let newBlobId = this._saveBlob(content, opts); + let newBlobId = this._saveBlob(content, unencryptedContentForHashCalculation, opts); if (newBlobId !== this.blobId || opts.forceSave) { this.blobId = newBlobId; @@ -168,7 +176,7 @@ class AbstractBeccaEntity { } /** @protected */ - _saveBlob(content, opts) { + _saveBlob(content, unencryptedContentForHashCalculation, opts) { let newBlobId; let blobNeedsInsert; @@ -176,7 +184,13 @@ class AbstractBeccaEntity { newBlobId = this.blobId || utils.randomBlobId(); blobNeedsInsert = true; } else { - newBlobId = utils.hashedBlobId(content); + /* + * We're using the unencrypted blob for the hash calculation, because otherwise the random IV would + * cause every content blob to be unique which would balloon the database size (esp. with revisioning). + * This has minor security implications (it's easy to infer that given content is shared between different + * notes/attachments, but the trade-off comes out clearly positive. + */ + newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation); blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); } diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index f74f43ac5..6a8d1e5ca 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -67,8 +67,7 @@ class BAttachment extends AbstractBeccaEntity { mime: this.mime, title: this.title, blobId: this.blobId, - isProtected: this.isProtected, - utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince + isProtected: this.isProtected }); } diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 1044a64b3..3657ce321 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -1580,6 +1580,10 @@ class BNote extends AbstractBeccaEntity { noteRevision.save(); // to generate noteRevisionId which is then used to save attachments for (const noteAttachment of this.getAttachments()) { + if (noteAttachment.utcDateScheduledForErasureSince) { + continue; + } + const revisionAttachment = noteAttachment.copy(); revisionAttachment.parentId = noteRevision.noteRevisionId; revisionAttachment.setContent(noteAttachment.getContent(), { diff --git a/src/public/app/widgets/type_widgets/abstract_text_type_widget.js b/src/public/app/widgets/type_widgets/abstract_text_type_widget.js index 6b96caac3..29399790a 100644 --- a/src/public/app/widgets/type_widgets/abstract_text_type_widget.js +++ b/src/public/app/widgets/type_widgets/abstract_text_type_widget.js @@ -44,8 +44,6 @@ export default class AbstractTextTypeWidget extends TypeWidget { } async parseFromImage($img) { - let noteId, viewScope; - const imgSrc = $img.prop("src"); const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//); diff --git a/src/public/app/widgets/type_widgets/options/other/attachment_erasure_timeout.js b/src/public/app/widgets/type_widgets/options/other/attachment_erasure_timeout.js index f6bd8e18a..8eb90ab9b 100644 --- a/src/public/app/widgets/type_widgets/options/other/attachment_erasure_timeout.js +++ b/src/public/app/widgets/type_widgets/options/other/attachment_erasure_timeout.js @@ -24,8 +24,8 @@ export default class AttachmentErasureTimeoutOptions extends OptionsWidget { this.$eraseUnusedAttachmentsAfterTimeInSeconds = this.$widget.find(".erase-unused-attachments-after-time-in-seconds"); this.$eraseUnusedAttachmentsAfterTimeInSeconds.on('change', () => this.updateOption('eraseUnusedImageAttachmentsAfterSeconds', this.$eraseUnusedAttachmentsAfterTimeInSeconds.val())); - this.$eraseDeletedNotesButton = this.$widget.find(".erase-unused-attachments-now-button"); - this.$eraseDeletedNotesButton.on('click', () => { + this.$eraseUnusedAttachmentsNowButton = this.$widget.find(".erase-unused-attachments-now-button"); + this.$eraseUnusedAttachmentsNowButton.on('click', () => { server.post('notes/erase-unused-attachments-now').then(() => { toastService.showMessage("Unused image attachments have been erased."); }); diff --git a/src/services/notes.js b/src/services/notes.js index 27d564cb9..504f6f3f8 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -346,7 +346,9 @@ function checkImageAttachments(note, content) { foundAttachmentIds.add(match[1]); } - for (const attachment of note.getAttachmentByRole('image')) { + const imageAttachments = note.getAttachmentByRole('image'); + + for (const attachment of imageAttachments) { const imageInContent = foundAttachmentIds.has(attachment.attachmentId); if (attachment.utcDateScheduledForErasureSince && imageInContent) { @@ -357,6 +359,20 @@ function checkImageAttachments(note, content) { attachment.save(); } } + + const existingAttachmentIds = new Set(imageAttachments.map(att => att.attachmentId)); + const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId)); + + for (const unknownAttachment of becca.getAttachments(unknownAttachmentIds)) { + // 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; + newAttachment.setContent(unknownAttachment.getContent(), { forceSave: true }); + + content = content.replace(`api/attachments/${unknownAttachment.attachmentId}/image`, `api/attachments/${newAttachment.attachmentId}/image`); + } + + return content; } @@ -581,7 +597,7 @@ function saveLinks(note, content) { content = findInternalLinks(content, foundLinks); content = findIncludeNoteLinks(content, foundLinks); - checkImageAttachments(note, content); + content = checkImageAttachments(note, content); } else if (note.type === 'relationMap') { findRelationMapLinks(content, foundLinks);