copying attachments WIP

This commit is contained in:
zadam 2023-04-25 00:01:58 +02:00
parent 49fb913eab
commit 330e7ac08e
7 changed files with 50 additions and 11 deletions

View File

@ -2,6 +2,7 @@
const sql = require("../services/sql"); const sql = require("../services/sql");
const NoteSet = require("../services/search/note_set"); 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. * 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; 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} */ /** @returns {BOption|null} */
getOption(name) { getOption(name) {
return this.options[name]; return this.options[name];

View File

@ -148,9 +148,17 @@ class AbstractBeccaEntity {
content = Buffer.isBuffer(content) ? content : Buffer.from(content); content = Buffer.isBuffer(content) ? content : Buffer.from(content);
} }
let unencryptedContentForHashCalculation = content;
if (this.isProtected) { if (this.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) { if (protectedSessionService.isProtectedSessionAvailable()) {
content = protectedSessionService.encrypt(content); 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 { else {
throw new Error(`Cannot update content of blob since we're out of protected session.`); throw new Error(`Cannot update content of blob since we're out of protected session.`);
@ -158,7 +166,7 @@ class AbstractBeccaEntity {
} }
sql.transactional(() => { sql.transactional(() => {
let newBlobId = this._saveBlob(content, opts); let newBlobId = this._saveBlob(content, unencryptedContentForHashCalculation, opts);
if (newBlobId !== this.blobId || opts.forceSave) { if (newBlobId !== this.blobId || opts.forceSave) {
this.blobId = newBlobId; this.blobId = newBlobId;
@ -168,7 +176,7 @@ class AbstractBeccaEntity {
} }
/** @protected */ /** @protected */
_saveBlob(content, opts) { _saveBlob(content, unencryptedContentForHashCalculation, opts) {
let newBlobId; let newBlobId;
let blobNeedsInsert; let blobNeedsInsert;
@ -176,7 +184,13 @@ class AbstractBeccaEntity {
newBlobId = this.blobId || utils.randomBlobId(); newBlobId = this.blobId || utils.randomBlobId();
blobNeedsInsert = true; blobNeedsInsert = true;
} else { } 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]); blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
} }

View File

@ -67,8 +67,7 @@ class BAttachment extends AbstractBeccaEntity {
mime: this.mime, mime: this.mime,
title: this.title, title: this.title,
blobId: this.blobId, blobId: this.blobId,
isProtected: this.isProtected, isProtected: this.isProtected
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince
}); });
} }

View File

@ -1580,6 +1580,10 @@ class BNote extends AbstractBeccaEntity {
noteRevision.save(); // to generate noteRevisionId which is then used to save attachments noteRevision.save(); // to generate noteRevisionId which is then used to save attachments
for (const noteAttachment of this.getAttachments()) { for (const noteAttachment of this.getAttachments()) {
if (noteAttachment.utcDateScheduledForErasureSince) {
continue;
}
const revisionAttachment = noteAttachment.copy(); const revisionAttachment = noteAttachment.copy();
revisionAttachment.parentId = noteRevision.noteRevisionId; revisionAttachment.parentId = noteRevision.noteRevisionId;
revisionAttachment.setContent(noteAttachment.getContent(), { revisionAttachment.setContent(noteAttachment.getContent(), {

View File

@ -44,8 +44,6 @@ export default class AbstractTextTypeWidget extends TypeWidget {
} }
async parseFromImage($img) { async parseFromImage($img) {
let noteId, viewScope;
const imgSrc = $img.prop("src"); const imgSrc = $img.prop("src");
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//); const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);

View File

@ -24,8 +24,8 @@ export default class AttachmentErasureTimeoutOptions extends OptionsWidget {
this.$eraseUnusedAttachmentsAfterTimeInSeconds = this.$widget.find(".erase-unused-attachments-after-time-in-seconds"); this.$eraseUnusedAttachmentsAfterTimeInSeconds = this.$widget.find(".erase-unused-attachments-after-time-in-seconds");
this.$eraseUnusedAttachmentsAfterTimeInSeconds.on('change', () => this.updateOption('eraseUnusedImageAttachmentsAfterSeconds', this.$eraseUnusedAttachmentsAfterTimeInSeconds.val())); this.$eraseUnusedAttachmentsAfterTimeInSeconds.on('change', () => this.updateOption('eraseUnusedImageAttachmentsAfterSeconds', this.$eraseUnusedAttachmentsAfterTimeInSeconds.val()));
this.$eraseDeletedNotesButton = this.$widget.find(".erase-unused-attachments-now-button"); this.$eraseUnusedAttachmentsNowButton = this.$widget.find(".erase-unused-attachments-now-button");
this.$eraseDeletedNotesButton.on('click', () => { this.$eraseUnusedAttachmentsNowButton.on('click', () => {
server.post('notes/erase-unused-attachments-now').then(() => { server.post('notes/erase-unused-attachments-now').then(() => {
toastService.showMessage("Unused image attachments have been erased."); toastService.showMessage("Unused image attachments have been erased.");
}); });

View File

@ -346,7 +346,9 @@ function checkImageAttachments(note, content) {
foundAttachmentIds.add(match[1]); 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); const imageInContent = foundAttachmentIds.has(attachment.attachmentId);
if (attachment.utcDateScheduledForErasureSince && imageInContent) { if (attachment.utcDateScheduledForErasureSince && imageInContent) {
@ -357,6 +359,20 @@ function checkImageAttachments(note, content) {
attachment.save(); 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 = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks); content = findIncludeNoteLinks(content, foundLinks);
checkImageAttachments(note, content); content = checkImageAttachments(note, content);
} }
else if (note.type === 'relationMap') { else if (note.type === 'relationMap') {
findRelationMapLinks(content, foundLinks); findRelationMapLinks(content, foundLinks);