1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

All: Improves deletion fail-safe so it is based on percentage of notes deleted. And display warning on sidebar.

This commit is contained in:
Laurent Cozic 2019-09-27 18:12:28 +00:00
parent 1836b9a0b0
commit c9098b0489
6 changed files with 60 additions and 37 deletions

View File

@ -10,7 +10,7 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data" mkdir -p "$BUILD_DIR/data"
if [[ $TEST_FILE != "" ]]; then if [[ $TEST_FILE != "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js) (cd "$ROOT_DIR" && NODE_ENV=testing npm test tests-build/$TEST_FILE.js)
exit exit
fi fi
@ -21,27 +21,27 @@ function finish {
trap finish EXIT trap finish EXIT
cd "$ROOT_DIR" cd "$ROOT_DIR"
npm test tests-build/ArrayUtils.js && \ NODE_ENV=testing npm test tests-build/ArrayUtils.js && \
npm test tests-build/encryption.js && \ NODE_ENV=testing npm test tests-build/encryption.js && \
npm test tests-build/EnexToMd.js && \ NODE_ENV=testing npm test tests-build/EnexToMd.js && \
npm test tests-build/HtmlToMd.js && \ NODE_ENV=testing npm test tests-build/HtmlToMd.js && \
npm test tests-build/markdownUtils.js && \ NODE_ENV=testing npm test tests-build/markdownUtils.js && \
npm test tests-build/models_BaseItem.js && \ NODE_ENV=testing npm test tests-build/models_BaseItem.js && \
npm test tests-build/models_Folder.js && \ NODE_ENV=testing npm test tests-build/models_Folder.js && \
npm test tests-build/models_ItemChange.js && \ NODE_ENV=testing npm test tests-build/models_ItemChange.js && \
npm test tests-build/models_Note.js && \ NODE_ENV=testing npm test tests-build/models_Note.js && \
npm test tests-build/models_Resource.js && \ NODE_ENV=testing npm test tests-build/models_Resource.js && \
npm test tests-build/models_Revision.js && \ NODE_ENV=testing npm test tests-build/models_Revision.js && \
npm test tests-build/models_Setting.js && \ NODE_ENV=testing npm test tests-build/models_Setting.js && \
npm test tests-build/models_Tag.js && \ NODE_ENV=testing npm test tests-build/models_Tag.js && \
npm test tests-build/pathUtils.js && \ NODE_ENV=testing npm test tests-build/pathUtils.js && \
npm test tests-build/services_InteropService.js && \ NODE_ENV=testing npm test tests-build/services_InteropService.js && \
npm test tests-build/services_KvStore.js && \ NODE_ENV=testing npm test tests-build/services_KvStore.js && \
npm test tests-build/services_ResourceService.js && \ NODE_ENV=testing npm test tests-build/services_ResourceService.js && \
npm test tests-build/services_rest_Api.js && \ NODE_ENV=testing npm test tests-build/services_rest_Api.js && \
npm test tests-build/services_SearchEngine.js && \ NODE_ENV=testing npm test tests-build/services_SearchEngine.js && \
npm test tests-build/services_Revision.js && \ NODE_ENV=testing npm test tests-build/services_Revision.js && \
npm test tests-build/StringUtils.js && \ NODE_ENV=testing npm test tests-build/StringUtils.js && \
npm test tests-build/TaskQueue.js && \ NODE_ENV=testing npm test tests-build/TaskQueue.js && \
npm test tests-build/synchronizer.js && \ NODE_ENV=testing npm test tests-build/synchronizer.js && \
npm test tests-build/urlUtils.js NODE_ENV=testing npm test tests-build/urlUtils.js

View File

@ -1481,19 +1481,27 @@ describe('Synchronizer', function() {
})); }));
it('should not wipe out user data when syncing with an empty target', asyncTest(async () => { it('should not wipe out user data when syncing with an empty target', asyncTest(async () => {
await Note.save({ title: 'ma note' }); for (let i = 0; i < 10; i++) await Note.save({ title: 'note' });
await Note.save({ title: 'mon autre note' });
await Note.save({ title: 'ma troisième note' });
Setting.setValue('sync.wipeOutFailSafe', true); Setting.setValue('sync.wipeOutFailSafe', true);
await synchronizer().start(); await synchronizer().start();
await fileApi().clearRoot(); // oops await fileApi().clearRoot(); // oops
await synchronizer().start(); await synchronizer().start();
expect((await Note.all()).length).toBe(3); // but since the fail-safe if on, the notes have not been deleted expect((await Note.all()).length).toBe(10); // but since the fail-safe if on, the notes have not been deleted
Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off
await synchronizer().start(); await synchronizer().start();
expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared
// Handle case where the sync target has been wiped out, then the user creates one note and sync.
for (let i = 0; i < 10; i++) await Note.save({ title: 'note' });
Setting.setValue('sync.wipeOutFailSafe', true);
await synchronizer().start();
await fileApi().clearRoot();
await Note.save({ title: 'ma note encore' });
await synchronizer().start();
expect((await Note.all()).length).toBe(11);
})); }));
}); });

View File

@ -5,6 +5,7 @@ const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError'); const JoplinError = require('lib/JoplinError');
const ArrayUtils = require('lib/ArrayUtils'); const ArrayUtils = require('lib/ArrayUtils');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js');
function requestCanBeRepeated(error) { function requestCanBeRepeated(error) {
const errorCode = typeof error === 'object' && error.code ? error.code : null; const errorCode = typeof error === 'object' && error.code ? error.code : null;
@ -262,13 +263,6 @@ async function basicDelta(path, getDirStatFn, options) {
}); });
newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path)); newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
// At this point statIdsCache contains the list of all the item IDs on the sync target.
// If it's empty, it's most likely a configuration error or bug. For example, if the
// user moves their Nextcloud directory, or if a network drive gets disconnected and
// returns an empty dir instead of an error. In that case, we don't wipe out the user
// data, unless they have switched off the fail-safe.
if (options.wipeOutFailSafe && !newContext.statIdsCache.length) throw new JoplinError('Fail-safe: The delta operation was interrupted because the sync target appears to be empty. To override this behaviour disable the "Wipe out fail-safe" option in the settings.', 'failSafe');
} }
let output = []; let output = [];
@ -322,6 +316,15 @@ async function basicDelta(path, getDirStatFn, options) {
} }
} }
const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0;
// If more than 90% of the notes are going to be deleted, it's most likely a
// configuration error or bug. For example, if the user moves their Nextcloud
// directory, or if a network drive gets disconnected and returns an empty dir
// instead of an error. In that case, we don't wipe out the user data, unless
// they have switched off the fail-safe.
if (options.wipeOutFailSafe && percentDeleted >= 0.90) throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe');
output = output.concat(deletedItems); output = output.concat(deletedItems);
} }

View File

@ -362,6 +362,10 @@ function shimInit() {
bridge().openExternal(url); bridge().openExternal(url);
}; };
shim.isTestingEnv = () => {
return process.env.NODE_ENV === 'testing';
};
shim.waitForFrame = () => {}; shim.waitForFrame = () => {};
} }

View File

@ -198,4 +198,8 @@ shim.loadCssFromJs = name => {
throw new Error('Not implemented'); throw new Error('Not implemented');
}; };
shim.isTestingEnv = () => {
return false;
};
module.exports = { shim }; module.exports = { shim };

View File

@ -715,7 +715,11 @@ class Synchronizer {
// Or it's a temporary issue that will be resolved on next sync. // Or it's a temporary issue that will be resolved on next sync.
this.logger().info(error.message); this.logger().info(error.message);
if (error.code === 'failSafe') this.logLastRequests(); if (error.code === 'failSafe') {
// Get the message to display on UI, but not in testing to avoid poluting stdout
if (!shim.isTestingEnv()) this.progressReport_.errors.push(error.message);
this.logLastRequests();
}
} else if (error.code === 'unknownItemType') { } else if (error.code === 'unknownItemType') {
this.progressReport_.errors.push(_('Unknown item type downloaded - please upgrade Joplin to the latest version')); this.progressReport_.errors.push(_('Unknown item type downloaded - please upgrade Joplin to the latest version'));
this.logger().error(error); this.logger().error(error);