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

All: Added fail-safe to prevent data from being wiped out when the sync target is empty

This commit is contained in:
Laurent Cozic 2019-09-25 18:40:04 +00:00
parent 3e5a9cdb97
commit 348efdd7b6
5 changed files with 32 additions and 2 deletions

View File

@ -1480,4 +1480,20 @@ describe('Synchronizer', function() {
expect((await decryptionWorker().decryptionDisabledItems()).length).toBe(0); expect((await decryptionWorker().decryptionDisabledItems()).length).toBe(0);
})); }));
it('should not wipe out user data when syncing with an empty target', asyncTest(async () => {
await Note.save({ title: 'ma note' });
await Note.save({ title: 'mon autre note' });
await Note.save({ title: 'ma troisième note' });
Setting.setValue('sync.wipeOutFailSafe', true);
await synchronizer().start();
await fileApi().clearRoot(); // oops
await synchronizer().start();
expect((await Note.all()).length).toBe(3); // but since the fail-safe if on, the notes have not been deleted
Setting.setValue('sync.wipeOutFailSafe', false); // Now switch it off
await synchronizer().start();
expect((await Note.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared
}));
}); });

View File

@ -135,7 +135,9 @@ async function switchClient(id) {
Setting.setConstant('resourceDir', resourceDir(id)); Setting.setConstant('resourceDir', resourceDir(id));
return Setting.load(); await Setting.load();
Setting.setValue('sync.wipeOutFailSafe', false); // To keep things simple, always disable fail-safe unless explicitely set in the test itself
} }
async function clearDatabase(id = null) { async function clearDatabase(id = null) {

View File

@ -247,6 +247,13 @@ 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 = [];

View File

@ -425,6 +425,8 @@ class Setting extends BaseModel {
label: () => _('Ignore TLS certificate errors'), label: () => _('Ignore TLS certificate errors'),
}, },
'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
'api.token': { value: null, type: Setting.TYPE_STRING, public: false }, 'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') }, 'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },

View File

@ -3,6 +3,7 @@ const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const ItemChange = require('lib/models/ItemChange.js'); const ItemChange = require('lib/models/ItemChange.js');
const Setting = require('lib/models/Setting.js');
const ResourceLocalState = require('lib/models/ResourceLocalState.js'); const ResourceLocalState = require('lib/models/ResourceLocalState.js');
const MasterKey = require('lib/models/MasterKey.js'); const MasterKey = require('lib/models/MasterKey.js');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
@ -508,6 +509,8 @@ class Synchronizer {
allItemIdsHandler: async () => { allItemIdsHandler: async () => {
return BaseItem.syncedItemIds(syncTargetId); return BaseItem.syncedItemIds(syncTargetId);
}, },
wipeOutFailSafe: Setting.value('sync.wipeOutFailSafe'),
}); });
let remotes = listResult.items; let remotes = listResult.items;
@ -695,7 +698,7 @@ class Synchronizer {
} }
} // DELTA STEP } // DELTA STEP
} catch (error) { } catch (error) {
if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice'].indexOf(error.code) >= 0) { if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey', 'processingPathTwice', 'failSafe'].indexOf(error.code) >= 0) {
// Only log an info statement for this since this is a common condition that is reported // Only log an info statement for this since this is a common condition that is reported
// in the application, and needs to be resolved by the user. // in the application, and needs to be resolved by the user.
// 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.