mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	got rid of the hot/cold blob, all blobs are "cold" now
This commit is contained in:
		| @@ -119,23 +119,10 @@ class AbstractBeccaEntity { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Hot entities keep stable blobId which is continuously updated and is not shared with other entities. |  | ||||||
|      * Cold entities can still update its blob, but the blobId will change (and new blob will be created). |  | ||||||
|      * Functionally this is the same, it's an optimization to avoid creating a new blob every second with auto saved |  | ||||||
|      * text notes. |  | ||||||
|      * |  | ||||||
|      * @protected |  | ||||||
|      */ |  | ||||||
|     _isHot() { |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** @protected */ |     /** @protected */ | ||||||
|     _setContent(content, opts = {}) { |     _setContent(content, opts = {}) { | ||||||
|         // client code asks to save entity even if blobId didn't change (something else was changed) |         // client code asks to save entity even if blobId didn't change (something else was changed) | ||||||
|         opts.forceSave = !!opts.forceSave; |         opts.forceSave = !!opts.forceSave; | ||||||
|         opts.forceCold = !!opts.forceCold; |  | ||||||
|         opts.forceFrontendReload = !!opts.forceFrontendReload; |         opts.forceFrontendReload = !!opts.forceFrontendReload; | ||||||
|  |  | ||||||
|         if (content === null || content === undefined) { |         if (content === null || content === undefined) { | ||||||
| @@ -149,7 +136,7 @@ class AbstractBeccaEntity { | |||||||
|             content = Buffer.isBuffer(content) ? content : Buffer.from(content); |             content = Buffer.isBuffer(content) ? content : Buffer.from(content); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const unencryptedContentForHashCalculation = this.getUnencryptedContentForHashCalculation(content); |         const unencryptedContentForHashCalculation = this.#getUnencryptedContentForHashCalculation(content); | ||||||
|  |  | ||||||
|         if (this.isProtected) { |         if (this.isProtected) { | ||||||
|             if (protectedSessionService.isProtectedSessionAvailable()) { |             if (protectedSessionService.isProtectedSessionAvailable()) { | ||||||
| @@ -160,16 +147,38 @@ class AbstractBeccaEntity { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         sql.transactional(() => { |         sql.transactional(() => { | ||||||
|             let newBlobId = this._saveBlob(content, unencryptedContentForHashCalculation, opts); |             const newBlobId = this.#saveBlob(content, unencryptedContentForHashCalculation, opts); | ||||||
|  |             const oldBlobId = this.blobId; | ||||||
|  |  | ||||||
|             if (newBlobId !== this.blobId || opts.forceSave) { |             if (newBlobId !== oldBlobId || opts.forceSave) { | ||||||
|                 this.blobId = newBlobId; |                 this.blobId = newBlobId; | ||||||
|                 this.save(); |                 this.save(); | ||||||
|  |  | ||||||
|  |                 if (newBlobId !== oldBlobId) { | ||||||
|  |                     this.#deleteBlobIfNoteUsed(oldBlobId); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getUnencryptedContentForHashCalculation(unencryptedContent) { |     #deleteBlobIfNoteUsed(blobId) { | ||||||
|  |         if (sql.getValue("SELECT 1 FROM notes WHERE blobId = ? LIMIT 1", [blobId])) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (sql.getValue("SELECT 1 FROM attachments WHERE blobId = ? LIMIT 1", [blobId])) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (sql.getValue("SELECT 1 FROM note_revisions WHERE blobId = ? LIMIT 1", [blobId])) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         sql.execute("DELETE FROM blobs WHERE blobId = ?", [blobId]); | ||||||
|  |         sql.execute("DELETE FROM entity_changes WHERE entityName = 'blobs' AND entityId = ?", [blobId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #getUnencryptedContentForHashCalculation(unencryptedContent) { | ||||||
|         if (this.isProtected) { |         if (this.isProtected) { | ||||||
|             // a "random" prefix make sure that the calculated hash/blobId is different for an encrypted note and decrypted |             // a "random" prefix make sure that the calculated hash/blobId is different for an encrypted note and decrypted | ||||||
|             const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_"; |             const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_"; | ||||||
| @@ -181,54 +190,47 @@ class AbstractBeccaEntity { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** @protected */ |     #saveBlob(content, unencryptedContentForHashCalculation, opts = {}) { | ||||||
|     _saveBlob(content, unencryptedContentForHashCalculation, opts = {}) { |         /* | ||||||
|         let newBlobId; |          * We're using the unencrypted blob for the hash calculation, because otherwise the random IV would | ||||||
|         let blobNeedsInsert; |          * 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). | ||||||
|  |          */ | ||||||
|  |         const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation); | ||||||
|  |         const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); | ||||||
|  |  | ||||||
|         if (this._isHot() && !opts.forceCold) { |         if (!blobNeedsInsert) { | ||||||
|             newBlobId = this.blobId || utils.randomBlobId(); |             return newBlobId; | ||||||
|             blobNeedsInsert = true; |  | ||||||
|         } else { |  | ||||||
|             /* |  | ||||||
|              * 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]); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (blobNeedsInsert) { |         const pojo = { | ||||||
|             const pojo = { |             blobId: newBlobId, | ||||||
|                 blobId: newBlobId, |             content: content, | ||||||
|                 content: content, |             dateModified: dateUtils.localNowDateTime(), | ||||||
|                 dateModified: dateUtils.localNowDateTime(), |             utcDateModified: dateUtils.utcNowDateTime() | ||||||
|                 utcDateModified: dateUtils.utcNowDateTime() |         }; | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             sql.upsert("blobs", "blobId", pojo); |         sql.upsert("blobs", "blobId", pojo); | ||||||
|  |  | ||||||
|             const hash = utils.hash(`${newBlobId}|${pojo.content.toString()}`); |         const hash = utils.hash(`${newBlobId}|${pojo.content.toString()}`); | ||||||
|  |  | ||||||
|             entityChangesService.addEntityChange({ |         entityChangesService.addEntityChange({ | ||||||
|                 entityName: 'blobs', |             entityName: 'blobs', | ||||||
|                 entityId: newBlobId, |             entityId: newBlobId, | ||||||
|                 hash: hash, |             hash: hash, | ||||||
|                 isErased: false, |             isErased: false, | ||||||
|                 utcDateChanged: pojo.utcDateModified, |             utcDateChanged: pojo.utcDateModified, | ||||||
|                 isSynced: true, |             isSynced: true, | ||||||
|                 // overriding componentId will cause frontend to think the change is coming from a different component |             // overriding componentId will cause frontend to think the change is coming from a different component | ||||||
|                 // and thus reload |             // and thus reload | ||||||
|                 componentId: opts.forceFrontendReload ? utils.randomString(10) : null |             componentId: opts.forceFrontendReload ? utils.randomString(10) : null | ||||||
|             }); |         }); | ||||||
|  |  | ||||||
|             eventService.emit(eventService.ENTITY_CHANGED, { |         eventService.emit(eventService.ENTITY_CHANGED, { | ||||||
|                 entityName: 'blobs', |             entityName: 'blobs', | ||||||
|                 entity: this |             entity: this | ||||||
|             }); |         }); | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return newBlobId; |         return newBlobId; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -118,7 +118,6 @@ class BAttachment extends AbstractBeccaEntity { | |||||||
|      * @param content |      * @param content | ||||||
|      * @param {object} [opts] |      * @param {object} [opts] | ||||||
|      * @param {object} [opts.forceSave=false] - will also save this BAttachment entity |      * @param {object} [opts.forceSave=false] - will also save this BAttachment entity | ||||||
|      * @param {object} [opts.forceCold=false] - blob has to be saved as cold |  | ||||||
|      * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload |      * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload | ||||||
|      */ |      */ | ||||||
|     setContent(content, opts) { |     setContent(content, opts) { | ||||||
|   | |||||||
| @@ -227,15 +227,10 @@ class BNote extends AbstractBeccaEntity { | |||||||
|         return JSON.parse(content); |         return JSON.parse(content); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     _isHot() { |  | ||||||
|         return ['text', 'code', 'relationMap', 'canvas', 'mermaid'].includes(this.type); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * @param content |      * @param content | ||||||
|      * @param {object} [opts] |      * @param {object} [opts] | ||||||
|      * @param {object} [opts.forceSave=false] - will also save this BNote entity |      * @param {object} [opts.forceSave=false] - will also save this BNote entity | ||||||
|      * @param {object} [opts.forceCold=false] - blob has to be saved as cold |  | ||||||
|      * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload |      * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload | ||||||
|      */ |      */ | ||||||
|     setContent(content, opts) { |     setContent(content, opts) { | ||||||
| @@ -1610,10 +1605,7 @@ class BNote extends AbstractBeccaEntity { | |||||||
|  |  | ||||||
|                 const revisionAttachment = noteAttachment.copy(); |                 const revisionAttachment = noteAttachment.copy(); | ||||||
|                 revisionAttachment.parentId = noteRevision.noteRevisionId; |                 revisionAttachment.parentId = noteRevision.noteRevisionId; | ||||||
|                 revisionAttachment.setContent(noteAttachment.getContent(), { |                 revisionAttachment.setContent(noteAttachment.getContent(), { forceSave: true }); | ||||||
|                     forceSave: true, |  | ||||||
|                     forceCold: true |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 // content is rewritten to point to the revision attachments |                 // content is rewritten to point to the revision attachments | ||||||
|                 noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); |                 noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); | ||||||
|   | |||||||
| @@ -784,7 +784,7 @@ components: | |||||||
|           readOnly: true |           readOnly: true | ||||||
|         blobId: |         blobId: | ||||||
|           type: string |           type: string | ||||||
|           description: ID of the blob object. For "cold" notes, this is effectively a content hash |           description: ID of the blob object which effectively serves as a content hash | ||||||
|         attributes: |         attributes: | ||||||
|           $ref: '#/components/schemas/AttributeList' |           $ref: '#/components/schemas/AttributeList' | ||||||
|           readOnly: true |           readOnly: true | ||||||
|   | |||||||
| @@ -892,6 +892,8 @@ function eraseAttachments(attachmentIdsToErase) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function eraseUnusedBlobs() { | function eraseUnusedBlobs() { | ||||||
|  |     // this method is rather defense in depth - in normal operation, the unused blobs should be erased immediately | ||||||
|  |     // after getting unused (handled in entity._setContent()) | ||||||
|     const unusedBlobIds = sql.getColumn(` |     const unusedBlobIds = sql.getColumn(` | ||||||
|         SELECT blobs.blobId |         SELECT blobs.blobId | ||||||
|         FROM blobs |         FROM blobs | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user