mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	copying attachments WIP
This commit is contained in:
		| @@ -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]; | ||||||
|   | |||||||
| @@ -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]); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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(), { | ||||||
|   | |||||||
| @@ -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_]+)\//); | ||||||
|   | |||||||
| @@ -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."); | ||||||
|             }); |             }); | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user