diff --git a/public/javascripts/dialogs/settings.js b/public/javascripts/dialogs/settings.js index 2eb385695..0afd93069 100644 --- a/public/javascripts/dialogs/settings.js +++ b/public/javascripts/dialogs/settings.js @@ -155,6 +155,9 @@ settings.addModule((async function () { settings.addModule((async function () { const forceFullSyncButton = $("#force-full-sync-button"); const fillSyncRowsButton = $("#fill-sync-rows-button"); + const anonymizeButton = $("#anonymize-button"); + const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button"); + const vacuumDatabaseButton = $("#vacuum-database-button"); forceFullSyncButton.click(async () => { await server.post('sync/force-full-sync'); @@ -168,11 +171,6 @@ settings.addModule((async function () { showMessage("Sync rows filled successfully"); }); - return {}; -})()); - -settings.addModule((async function () { - const anonymizeButton = $("#anonymize-button"); anonymizeButton.click(async () => { await server.post('anonymization/anonymize'); @@ -180,5 +178,19 @@ settings.addModule((async function () { showMessage("Created anonymized database"); }); + cleanupSoftDeletedButton.click(async () => { + if (confirm("Do you really want to clean up soft-deleted items?")) { + await server.post('cleanup/cleanup-soft-deleted-items'); + + showMessage("Soft deleted items have been cleaned up"); + } + }); + + vacuumDatabaseButton.click(async () => { + await server.post('cleanup/vacuum-database'); + + showMessage("Database has been vacuumed"); + }); + return {}; })()); \ No newline at end of file diff --git a/routes/api/cleanup.js b/routes/api/cleanup.js new file mode 100644 index 000000000..7b2783fb2 --- /dev/null +++ b/routes/api/cleanup.js @@ -0,0 +1,43 @@ +"use strict"; + +const express = require('express'); +const router = express.Router(); +const sql = require('../../services/sql'); +const utils = require('../../services/utils'); +const sync_table = require('../../services/sync_table'); + +router.post('/cleanup-soft-deleted-items', async (req, res, next) => { + await sql.doInTransaction(async () => { + const noteIdsToDelete = await sql.getFlattenedResults("SELECT note_id FROM notes WHERE is_deleted = 1"); + const noteIdsSql = noteIdsToDelete + .map(noteId => "'" + utils.sanitizeSql(noteId) + "'") + .join(', '); + + console.log("Note IDS for deletion", noteIdsSql); + + await sql.execute(`DELETE FROM event_log WHERE note_id IN (${noteIdsSql})`); + + await sql.execute(`DELETE FROM notes_history WHERE note_id IN (${noteIdsSql})`); + + await sql.execute("DELETE FROM notes_tree WHERE is_deleted = 1"); + + await sql.execute("DELETE FROM notes WHERE is_deleted = 1"); + + await sql.execute("DELETE FROM recent_notes"); + + await sync_table.cleanupSyncRowsForMissingEntities("notes", "note_id"); + await sync_table.cleanupSyncRowsForMissingEntities("notes_tree", "note_tree_id"); + await sync_table.cleanupSyncRowsForMissingEntities("notes_history", "note_history_id"); + await sync_table.cleanupSyncRowsForMissingEntities("recent_notes", "note_tree_id"); + }); + + res.send({}); +}); + +router.post('/vacuum-database', async (req, res, next) => { + await sql.execute("VACUUM"); + + res.send({}); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/api/import.js b/routes/api/import.js index a9b629dd6..170d99e36 100644 --- a/routes/api/import.js +++ b/routes/api/import.js @@ -2,7 +2,6 @@ const express = require('express'); const router = express.Router(); -const rimraf = require('rimraf'); const fs = require('fs'); const sql = require('../../services/sql'); const data_dir = require('../../services/data_dir'); diff --git a/routes/api/sync.js b/routes/api/sync.js index c5bbeab58..86a16a561 100644 --- a/routes/api/sync.js +++ b/routes/api/sync.js @@ -5,11 +5,10 @@ const router = express.Router(); const auth = require('../../services/auth'); const sync = require('../../services/sync'); const syncUpdate = require('../../services/sync_update'); +const sync_table = require('../../services/sync_table'); const sql = require('../../services/sql'); const options = require('../../services/options'); const content_hash = require('../../services/content_hash'); -const utils = require('../../services/utils'); -const log = require('../../services/log'); router.get('/check', auth.checkApiAuth, async (req, res, next) => { res.send({ @@ -22,39 +21,12 @@ router.post('/now', auth.checkApiAuth, async (req, res, next) => { res.send(await sync.sync()); }); -async function fillSyncRows(entityName, entityKey) { - // cleanup sync rows for missing entities - await sql.execute(` - DELETE - FROM sync - WHERE sync.entity_name = '${entityName}' - AND sync.entity_id NOT IN (SELECT ${entityKey} FROM ${entityName})`); - - const entityIds = await sql.getFlattenedResults(`SELECT ${entityKey} FROM ${entityName}`); - - for (const entityId of entityIds) { - const existingRows = await sql.getSingleValue("SELECT COUNT(id) FROM sync WHERE entity_name = ? AND entity_id = ?", [entityName, entityId]); - - // we don't want to replace existing entities (which would effectively cause full resync) - if (existingRows === 0) { - log.info(`Creating missing sync record for ${entityName} ${entityId}`); - - await sql.insert("sync", { - entity_name: entityName, - entity_id: entityId, - source_id: "SYNC_FILL", - sync_date: utils.nowDate() - }); - } - } -} - router.post('/fill-sync-rows', auth.checkApiAuth, async (req, res, next) => { await sql.doInTransaction(async () => { - await fillSyncRows("notes", "note_id"); - await fillSyncRows("notes_tree", "note_tree_id"); - await fillSyncRows("notes_history", "note_history_id"); - await fillSyncRows("recent_notes", "note_tree_id"); + await sync_table.fillSyncRows("notes", "note_id"); + await sync_table.fillSyncRows("notes_tree", "note_tree_id"); + await sync_table.fillSyncRows("notes_history", "note_history_id"); + await sync_table.fillSyncRows("recent_notes", "note_tree_id"); }); res.send({}); diff --git a/routes/routes.js b/routes/routes.js index ccb76f3ee..5617f4a2e 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -23,6 +23,7 @@ const importRoute = require('./api/import'); const setupApiRoute = require('./api/setup'); const sqlRoute = require('./api/sql'); const anonymizationRoute = require('./api/anonymization'); +const cleanupRoute = require('./api/cleanup'); function register(app) { app.use('/', indexRoute); @@ -49,6 +50,7 @@ function register(app) { app.use('/api/setup', setupApiRoute); app.use('/api/sql', sqlRoute); app.use('/api/anonymization', anonymizationRoute); + app.use('/api/cleanup', cleanupRoute); } module.exports = { diff --git a/services/sync_table.js b/services/sync_table.js index b642b08e0..be95a8e11 100644 --- a/services/sync_table.js +++ b/services/sync_table.js @@ -2,6 +2,7 @@ const sql = require('./sql'); const source_id = require('./source_id'); const utils = require('./utils'); const sync_setup = require('./sync_setup'); +const log = require('./log'); async function addNoteSync(noteId, sourceId) { await addEntitySync("notes", noteId, sourceId) @@ -42,11 +43,43 @@ async function addEntitySync(entityName, entityId, sourceId) { } } +async function cleanupSyncRowsForMissingEntities(entityName, entityKey) { + await sql.execute(` + DELETE + FROM sync + WHERE sync.entity_name = '${entityName}' + AND sync.entity_id NOT IN (SELECT ${entityKey} FROM ${entityName})`); +} + +async function fillSyncRows(entityName, entityKey) { + await cleanupSyncRowsForMissingEntities(entityName, entityKey); + + const entityIds = await sql.getFlattenedResults(`SELECT ${entityKey} FROM ${entityName}`); + + for (const entityId of entityIds) { + const existingRows = await sql.getSingleValue("SELECT COUNT(id) FROM sync WHERE entity_name = ? AND entity_id = ?", [entityName, entityId]); + + // we don't want to replace existing entities (which would effectively cause full resync) + if (existingRows === 0) { + log.info(`Creating missing sync record for ${entityName} ${entityId}`); + + await sql.insert("sync", { + entity_name: entityName, + entity_id: entityId, + source_id: "SYNC_FILL", + sync_date: utils.nowDate() + }); + } + } +} + module.exports = { addNoteSync, addNoteTreeSync, addNoteReorderingSync, addNoteHistorySync, addOptionsSync, - addRecentNoteSync + addRecentNoteSync, + cleanupSyncRowsForMissingEntities, + fillSyncRows }; \ No newline at end of file diff --git a/services/utils.js b/services/utils.js index c861d1e52..ae166e566 100644 --- a/services/utils.js +++ b/services/utils.js @@ -74,6 +74,11 @@ function getDateTimeForFile() { return new Date().toISOString().substr(0, 19).replace(/:/g, ''); } +function sanitizeSql(str) { + // should be improved or usage eliminated + return str.replace(/'/g, "\\'"); +} + module.exports = { randomSecureToken, randomString, @@ -89,5 +94,6 @@ module.exports = { isElectron, hash, isEmptyOrWhitespace, - getDateTimeForFile + getDateTimeForFile, + sanitizeSql }; \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index 0256a0972..1af957a74 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -183,8 +183,7 @@
This action will create a new copy of the database and anonymise it (remove all note content and leave only structure and metadata) + for sharing online for debugging purposes without fear of leaking your personal data.
+ +