mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-29 02:28:57 +01:00 
			
		
		
		
	erasing rows of deleted entities
This commit is contained in:
		
							parent
							
								
									20c7c657da
								
							
						
					
					
						commit
						248fa780e8
					
				| @ -0,0 +1 @@ | ||||
| UPDATE options SET name = 'eraseNotesAfterTimeInSeconds' WHERE name = 'eraseNotesAfterTimeInSeconds'; | ||||
| @ -42,14 +42,14 @@ const TPL = ` | ||||
| <div> | ||||
|     <h4>Note erasure timeout</h4> | ||||
| 
 | ||||
|     <p>Deleted notes are at first only marked as deleted and it is possible to recover them  | ||||
|     <p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them  | ||||
|     from Recent Notes dialog. After a period of time, deleted notes are "erased" which means  | ||||
|     their content is not recoverable anymore. This setting allows you to configure the length  | ||||
|     of the period between deleting and erasing the note.</p> | ||||
| 
 | ||||
|     <div class="form-group"> | ||||
|         <label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label> | ||||
|         <input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0"> | ||||
|         <label for="erase-entities-after-time-in-seconds">Erase notes after X seconds</label> | ||||
|         <input class="form-control" id="erase-entities-after-time-in-seconds" type="number" min="0"> | ||||
|     </div> | ||||
|      | ||||
|     <p>You can also trigger erasing manually:</p> | ||||
| @ -111,12 +111,12 @@ export default class ProtectedSessionOptions { | ||||
|             this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', ')); | ||||
|         } | ||||
| 
 | ||||
|         this.$eraseNotesAfterTimeInSeconds = $("#erase-notes-after-time-in-seconds"); | ||||
|         this.$eraseEntitiesAfterTimeInSeconds = $("#erase-entities-after-time-in-seconds"); | ||||
| 
 | ||||
|         this.$eraseNotesAfterTimeInSeconds.on('change', () => { | ||||
|             const eraseNotesAfterTimeInSeconds = this.$eraseNotesAfterTimeInSeconds.val(); | ||||
|         this.$eraseEntitiesAfterTimeInSeconds.on('change', () => { | ||||
|             const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val(); | ||||
| 
 | ||||
|             server.put('options', { 'eraseNotesAfterTimeInSeconds': eraseNotesAfterTimeInSeconds }).then(() => { | ||||
|             server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => { | ||||
|                 toastService.showMessage("Options change have been saved."); | ||||
|             }); | ||||
| 
 | ||||
| @ -173,7 +173,7 @@ export default class ProtectedSessionOptions { | ||||
|         this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true'); | ||||
|         this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']); | ||||
| 
 | ||||
|         this.$eraseNotesAfterTimeInSeconds.val(options['eraseNotesAfterTimeInSeconds']); | ||||
|         this.$eraseEntitiesAfterTimeInSeconds.val(options['eraseEntitiesAfterTimeInSeconds']); | ||||
|         this.$protectedSessionTimeout.val(options['protectedSessionTimeout']); | ||||
|         this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']); | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,7 @@ const attributes = require('../../services/attributes'); | ||||
| // options allowed to be updated directly in options dialog
 | ||||
| const ALLOWED_OPTIONS = new Set([ | ||||
|     'username', // not exposed for update (not harmful anyway), needed for reading
 | ||||
|     'eraseNotesAfterTimeInSeconds', | ||||
|     'eraseEntitiesAfterTimeInSeconds', | ||||
|     'protectedSessionTimeout', | ||||
|     'noteRevisionSnapshotTimeInterval', | ||||
|     'zoomFactor', | ||||
|  | ||||
| @ -4,7 +4,7 @@ const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
| const {TRILIUM_DATA_DIR} = require('./data_dir'); | ||||
| 
 | ||||
| const APP_DB_VERSION = 173; | ||||
| const APP_DB_VERSION = 175; | ||||
| const SYNC_VERSION = 17; | ||||
| const CLIPPER_PROTOCOL_VERSION = "1.0"; | ||||
| 
 | ||||
|  | ||||
| @ -673,61 +673,90 @@ function scanForLinks(note) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) { | ||||
|     if (eraseNotesAfterTimeInSeconds === null) { | ||||
|         eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds'); | ||||
|     } | ||||
| 
 | ||||
|     const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000); | ||||
| 
 | ||||
|     const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND isErased = 0 AND notes.utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]); | ||||
| 
 | ||||
| function eraseNotes(noteIdsToErase) { | ||||
|     if (noteIdsToErase.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // it's better to not use repository for this because:
 | ||||
|     // - it would complain about saving protected notes out of protected session
 | ||||
|     // - we don't want these changes to be synced (since they are done on all instances anyway)
 | ||||
|     // - we don't want change the hash since this erasing happens on each instance separately
 | ||||
|     //   and changing the hash would fire up the sync errors temporarily
 | ||||
|     sql.executeMany(`DELETE FROM notes WHERE noteId IN (???)`, noteIdsToErase); | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'notes' AND entityId IN (???)`, noteIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(` | ||||
|         UPDATE notes  | ||||
|         SET title = '[erased]', | ||||
|             isProtected = 0, | ||||
|             isErased = 1 | ||||
|         WHERE noteId IN (???)`, noteIdsToErase);
 | ||||
|     sql.executeMany(`DELETE FROM note_contents WHERE noteId IN (???)`, noteIdsToErase); | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'note_contents' AND entityId IN (???)`, noteIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(` | ||||
|         UPDATE note_contents  | ||||
|         SET content = NULL  | ||||
|         WHERE noteId IN (???)`, noteIdsToErase);
 | ||||
|     // we also need to erase all "dependent" entities of the erased notes
 | ||||
|     const branchIdsToErase = sql.getManyRows(`SELECT branchId FROM branches WHERE noteId IN (???)`, noteIdsToErase) | ||||
|         .map(row => row.branchId); | ||||
| 
 | ||||
|     // deleting first contents since the WHERE relies on isErased = 0
 | ||||
|     sql.executeMany(` | ||||
|         UPDATE note_revision_contents | ||||
|         SET content = NULL | ||||
|         WHERE noteRevisionId IN  | ||||
|             (SELECT noteRevisionId FROM note_revisions WHERE isErased = 0 AND noteId IN (???))`, noteIdsToErase);
 | ||||
|     eraseBranches(branchIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(` | ||||
|         UPDATE note_revisions  | ||||
|         SET isErased = 1, | ||||
|             title = NULL | ||||
|         WHERE isErased = 0 AND noteId IN (???)`, noteIdsToErase);
 | ||||
|     const attributeIdsToErase = sql.getManyRows(`SELECT attributeId FROM attributes WHERE noteId IN (???)`, noteIdsToErase) | ||||
|         .map(row => row.attributeId); | ||||
| 
 | ||||
|     sql.executeMany(` | ||||
|         UPDATE attributes  | ||||
|         SET name = 'deleted', | ||||
|             value = '' | ||||
|         WHERE noteId IN (???)`, noteIdsToErase);
 | ||||
|     eraseAttributes(attributeIdsToErase); | ||||
| 
 | ||||
|     const noteRevisionIdsToErase = sql.getManyRows(`SELECT noteRevisionId FROM note_revisions WHERE noteId IN (???)`, noteIdsToErase) | ||||
|         .map(row => row.noteRevisionId); | ||||
| 
 | ||||
|     eraseNoteRevisions(noteRevisionIdsToErase); | ||||
| 
 | ||||
|     log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`); | ||||
| } | ||||
| 
 | ||||
| function eraseBranches(branchIdsToErase) { | ||||
|     if (branchIdsToErase.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     sql.executeMany(`DELETE FROM branches WHERE branchId IN (???)`, branchIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'branches' AND entityId IN (???)`, branchIdsToErase); | ||||
| } | ||||
| 
 | ||||
| function eraseAttributes(attributeIdsToErase) { | ||||
|     if (attributeIdsToErase.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     sql.executeMany(`DELETE FROM attributes WHERE attributeId IN (???)`, attributeIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'attributes' AND entityId IN (???)`, attributeIdsToErase); | ||||
| } | ||||
| 
 | ||||
| function eraseNoteRevisions(noteRevisionIdsToErase) { | ||||
|     if (noteRevisionIdsToErase.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     sql.executeMany(`DELETE FROM note_revisions WHERE noteRevisionId IN (???)`, noteRevisionIdsToErase); | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'note_revisions' AND entityId IN (???)`, noteRevisionIdsToErase); | ||||
| 
 | ||||
|     sql.executeMany(`DELETE FROM note_revision_contents WHERE noteRevisionId IN (???)`, noteRevisionIdsToErase); | ||||
|     sql.executeMany(`UPDATE entity_changes SET isErased = 1 WHERE entityName = 'note_revision_contents' AND entityId IN (???)`, noteRevisionIdsToErase); | ||||
| } | ||||
| 
 | ||||
| function eraseDeletedEntities(eraseEntitiesAfterTimeInSeconds = null) { | ||||
|     if (eraseEntitiesAfterTimeInSeconds === null) { | ||||
|         eraseEntitiesAfterTimeInSeconds = optionService.getOptionInt('eraseEntitiesAfterTimeInSeconds'); | ||||
|     } | ||||
| 
 | ||||
|     const cutoffDate = new Date(Date.now() - eraseEntitiesAfterTimeInSeconds * 1000); | ||||
| 
 | ||||
|     const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]); | ||||
| 
 | ||||
|     eraseNotes(noteIdsToErase); | ||||
| 
 | ||||
|     const branchIdsToErase = sql.getColumn("SELECT branchId FROM branches WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]); | ||||
| 
 | ||||
|     eraseBranches(branchIdsToErase); | ||||
| 
 | ||||
|     const attributeIdsToErase = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 1 AND utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]); | ||||
| 
 | ||||
|     eraseAttributes(attributeIdsToErase); | ||||
| } | ||||
| 
 | ||||
| function eraseDeletedNotesNow() { | ||||
|     eraseDeletedNotes(0); | ||||
|     eraseDeletedEntities(0); | ||||
| } | ||||
| 
 | ||||
| // do a replace in str - all keys should be replaced by the corresponding values
 | ||||
| @ -836,9 +865,9 @@ function getNoteIdMapping(origNote) { | ||||
| 
 | ||||
| sqlInit.dbReady.then(() => { | ||||
|     // first cleanup kickoff 5 minutes after startup
 | ||||
|     setTimeout(cls.wrap(() => eraseDeletedNotes()), 5 * 60 * 1000); | ||||
|     setTimeout(cls.wrap(() => eraseDeletedEntities()), 5 * 60 * 1000); | ||||
| 
 | ||||
|     setInterval(cls.wrap(() => eraseDeletedNotes()), 4 * 3600 * 1000); | ||||
|     setInterval(cls.wrap(() => eraseDeletedEntities()), 4 * 3600 * 1000); | ||||
| }); | ||||
| 
 | ||||
| module.exports = { | ||||
|  | ||||
| @ -82,7 +82,7 @@ const defaultOptions = [ | ||||
|     { name: 'rightPaneWidth', value: '25', isSynced: false }, | ||||
|     { name: 'rightPaneVisible', value: 'true', isSynced: false }, | ||||
|     { name: 'nativeTitleBarVisible', value: 'false', isSynced: false }, | ||||
|     { name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days
 | ||||
|     { name: 'eraseEntitiesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days
 | ||||
|     { name: 'hideArchivedNotes_main', value: 'false', isSynced: false }, | ||||
|     { name: 'hideIncludedImages_main', value: 'true', isSynced: false }, | ||||
|     { name: 'attributeListExpanded', value: 'false', isSynced: false }, | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| const sql = require('./sql'); | ||||
| const log = require('./log'); | ||||
| const entityChangesService = require('./entity_changes.js'); | ||||
| const eventService = require('./events'); | ||||
| 
 | ||||
| @ -9,22 +8,15 @@ function updateEntity(entityChange, entity, sourceId) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     const {entityName, hash} = entityChange; | ||||
|     let updated; | ||||
| 
 | ||||
|     if (entityName === 'note_reordering') { | ||||
|         updated = updateNoteReordering(entityChange, entity, sourceId); | ||||
|     } | ||||
|     else { | ||||
|         updated = updateNormalEntity(entityChange, entity, sourceId); | ||||
|     } | ||||
|     const updated = entityChange.entityName === 'note_reordering' | ||||
|         ? updateNoteReordering(entityChange, entity, sourceId) | ||||
|         : updateNormalEntity(entityChange, entity, sourceId); | ||||
| 
 | ||||
|     // currently making exception for protected notes and note revisions because here
 | ||||
|     // the title and content are not available decrypted as listeners would expect
 | ||||
|     if (updated && | ||||
|         (!['notes', 'note_contents', 'note_revisions', 'note_revision_contents'].includes(entityName) || !entity.isProtected)) { | ||||
|     if (updated && !entity.isProtected) { | ||||
|         eventService.emit(eventService.ENTITY_SYNCED, { | ||||
|             entityName, | ||||
|             entityName: entityChange.entityName, | ||||
|             entity | ||||
|         }); | ||||
|     } | ||||
| @ -42,14 +34,7 @@ function updateNormalEntity(entityChange, entity, sourceId) { | ||||
|         || hash !== entityChange.hash // sync error, we should still update
 | ||||
|     ) { | ||||
|         if (['note_contents', 'note_revision_contents'].includes(entityChange.entityName)) { | ||||
|             // we always use Buffer object which is different from normal saving - there we use simple string type for "string notes"
 | ||||
|             // the problem is that in general it's not possible to whether a note_content is string note or note (syncs can arrive out of order)
 | ||||
|             entity.content = entity.content === null ? null : Buffer.from(entity.content, 'base64'); | ||||
| 
 | ||||
|             if (entity.content && entity.content.byteLength === 0) { | ||||
|                 // there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
 | ||||
|                 entity.content = ""; | ||||
|             } | ||||
|             entity.content = handleContent(entity.content); | ||||
|         } | ||||
| 
 | ||||
|         sql.transactional(() => { | ||||
| @ -76,6 +61,19 @@ function updateNoteReordering(entityChange, entity, sourceId) { | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| function handleContent(content) { | ||||
|     // we always use Buffer object which is different from normal saving - there we use simple string type for "string notes"
 | ||||
|     // the problem is that in general it's not possible to whether a note_content is string note or note (syncs can arrive out of order)
 | ||||
|     content = content === null ? null : Buffer.from(content, 'base64'); | ||||
| 
 | ||||
|     if (content && content.byteLength === 0) { | ||||
|         // there seems to be a bug which causes empty buffer to be stored as NULL which is then picked up as inconsistency
 | ||||
|         content = ""; | ||||
|     } | ||||
| 
 | ||||
|     return content; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     updateEntity | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam