diff --git a/src/public/app/dialogs/options/advanced.js b/src/public/app/dialogs/options/advanced.js
index d33b94ba8..c446f1e01 100644
--- a/src/public/app/dialogs/options/advanced.js
+++ b/src/public/app/dialogs/options/advanced.js
@@ -24,6 +24,13 @@ const TPL = `
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.
+This will rebuild database which will typically result in smaller database file. No data will be actually changed.
@@ -37,6 +44,7 @@ export default class AdvancedOptions {
this.$forceFullSyncButton = $("#force-full-sync-button");
this.$fillSyncRowsButton = $("#fill-sync-rows-button");
this.$anonymizeButton = $("#anonymize-button");
+ this.$backupDatabaseButton = $("#backup-database-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button");
@@ -58,16 +66,22 @@ export default class AdvancedOptions {
toastService.showMessage("Created anonymized database");
});
+ this.$backupDatabaseButton.on('click', async () => {
+ const {backupFile} = await server.post('database/backup-database');
+
+ toastService.showMessage("Database has been backed up to " + backupFile, 10000);
+ });
+
this.$vacuumDatabaseButton.on('click', async () => {
- await server.post('cleanup/vacuum-database');
+ await server.post('database/vacuum-database');
toastService.showMessage("Database has been vacuumed");
});
this.$findAndFixConsistencyIssuesButton.on('click', async () => {
- await server.post('cleanup/find-and-fix-consistency-issues');
+ await server.post('database/find-and-fix-consistency-issues');
toastService.showMessage("Consistency issues should be fixed.");
});
}
-}
\ No newline at end of file
+}
diff --git a/src/public/app/widgets/type_widgets/file.js b/src/public/app/widgets/type_widgets/file.js
index 7ec5e43af..275138781 100644
--- a/src/public/app/widgets/type_widgets/file.js
+++ b/src/public/app/widgets/type_widgets/file.js
@@ -5,17 +5,23 @@ import TypeWidget from "./type_widget.js";
const TPL = `
+
+
- Note ID: |
+ Note ID: |
|
- Original file name: |
+ Original file name: |
|
- File type: |
+ File type: |
|
- File size: |
+ File size: |
|
@@ -94,7 +100,7 @@ export default class FileTypeWidget extends TypeWidget {
toastService.showError("Upload of a new file revision failed.");
}
});
-
+
return this.$widget;
}
@@ -130,4 +136,4 @@ export default class FileTypeWidget extends TypeWidget {
getFileUrl() {
return utils.getUrlForDownload("api/notes/" + this.noteId + "/download");
}
-}
\ No newline at end of file
+}
diff --git a/src/routes/api/cleanup.js b/src/routes/api/database.js
similarity index 71%
rename from src/routes/api/cleanup.js
rename to src/routes/api/database.js
index 877a90898..729540221 100644
--- a/src/routes/api/cleanup.js
+++ b/src/routes/api/database.js
@@ -2,8 +2,15 @@
const sql = require('../../services/sql');
const log = require('../../services/log');
+const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks');
+async function backupDatabase() {
+ return {
+ backupFile: await backupService.backupNow("now")
+ };
+}
+
async function vacuumDatabase() {
await sql.execute("VACUUM");
@@ -15,6 +22,7 @@ async function findAndFixConsistencyIssues() {
}
module.exports = {
+ backupDatabase,
vacuumDatabase,
findAndFixConsistencyIssues
-};
\ No newline at end of file
+};
diff --git a/src/routes/api/login.js b/src/routes/api/login.js
index 6bca0212a..2bd88efec 100644
--- a/src/routes/api/login.js
+++ b/src/routes/api/login.js
@@ -16,7 +16,7 @@ const ApiToken = require('../../entities/api_token');
async function loginSync(req) {
if (!await sqlInit.schemaExists()) {
- return [400, { message: "DB schema does not exist, can't sync." }];
+ return [500, { message: "DB schema does not exist, can't sync." }];
}
const timestampStr = req.body.timestamp;
@@ -27,7 +27,7 @@ async function loginSync(req) {
// login token is valid for 5 minutes
if (Math.abs(timestamp.getTime() - now.getTime()) > 5 * 60 * 1000) {
- return [400, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
+ return [401, { message: 'Auth request time is out of sync, please check that both client and server have correct time.' }];
}
const syncVersion = req.body.syncVersion;
@@ -102,4 +102,4 @@ module.exports = {
loginSync,
loginToProtectedSession,
token
-};
\ No newline at end of file
+};
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 6cbead5c3..8e231f006 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -25,7 +25,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');
+const databaseRoute = require('./api/database');
const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
@@ -223,10 +223,13 @@ function register(app) {
apiRoute(POST, '/api/sql/execute', sqlRoute.execute);
apiRoute(POST, '/api/anonymization/anonymize', anonymizationRoute.anonymize);
- // VACUUM requires execution outside of transaction
- route(POST, '/api/cleanup/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.vacuumDatabase, apiResultHandler, false);
+ // backup requires execution outside of transaction
+ route(POST, '/api/database/backup-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.backupDatabase, apiResultHandler, false);
- route(POST, '/api/cleanup/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], cleanupRoute.findAndFixConsistencyIssues, apiResultHandler, false);
+ // VACUUM requires execution outside of transaction
+ route(POST, '/api/database/vacuum-database', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.vacuumDatabase, apiResultHandler, false);
+
+ route(POST, '/api/database/find-and-fix-consistency-issues', [auth.checkApiAuthOrElectron, csrfMiddleware], databaseRoute.findAndFixConsistencyIssues, apiResultHandler, false);
apiRoute(POST, '/api/script/exec', scriptRoute.exec);
apiRoute(POST, '/api/script/run/:noteId', scriptRoute.run);
@@ -268,4 +271,4 @@ function register(app) {
module.exports = {
register
-};
\ No newline at end of file
+};
diff --git a/src/services/backup.js b/src/services/backup.js
index 59ece88b0..063a27d27 100644
--- a/src/services/backup.js
+++ b/src/services/backup.js
@@ -29,13 +29,38 @@ async function periodBackup(optionName, fileName, periodInSeconds) {
}
async function backupNow(name) {
+ const sql = require('./sql');
+
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state
- await syncMutexService.doExclusively(async () => {
+ return await syncMutexService.doExclusively(async () => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
- fs.copySync(dataDir.DOCUMENT_PATH, backupFile);
+ try {
+ fs.unlinkSync(backupFile);
+ }
+ catch (e) {} // unlink throws exception if the file did not exist
- log.info("Created backup at " + backupFile);
+ let success = false;
+ let attemptCount = 0
+
+ for (; attemptCount < 50 && !success; attemptCount++) {
+ try {
+ await sql.executeNoWrap(`VACUUM INTO '${backupFile}'`);
+ success++;
+ }
+ catch (e) {}
+ // we re-try since VACUUM is very picky and it can't run if there's any other query currently running
+ // which is difficult to guarantee so we just re-try
+ }
+
+ if (attemptCount === 10) {
+ log.error(`Creating backup ${backupFile} failed`);
+ }
+ else {
+ log.info("Created backup at " + backupFile);
+ }
+
+ return backupFile;
});
}
@@ -52,4 +77,4 @@ sqlInit.dbReady.then(() => {
module.exports = {
backupNow
-};
\ No newline at end of file
+};
diff --git a/src/services/sql.js b/src/services/sql.js
index 179b35103..6f5a8d1e7 100644
--- a/src/services/sql.js
+++ b/src/services/sql.js
@@ -153,6 +153,10 @@ async function execute(query, params = []) {
return await wrap(async db => db.run(query, ...params), query);
}
+async function executeNoWrap(query, params = []) {
+ await dbConnection.run(query, ...params);
+}
+
async function executeMany(query, params) {
// essentially just alias
await getManyRows(query, params);
@@ -264,6 +268,7 @@ module.exports = {
getMap,
getColumn,
execute,
+ executeNoWrap,
executeMany,
executeScript,
transactional,