Skip to content

Commit e27dc07

Browse files
authored
Merge pull request #1902 from floccusaddon/feat/addition-failsafe
feat(AdditionFailsafe): Extend failsafe mechanism to prevent creation of excessive amounts of bookmarks
2 parents 41b8f64 + c668101 commit e27dc07

File tree

7 files changed

+235
-20
lines changed

7 files changed

+235
-20
lines changed

_locales/en/messages.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@
125125
"Error042": {
126126
"message": "E042: Remote bookmarks file size could not be retrieved. It is impossible to verify that the bookmarks file was downloaded in full. If this error persists please contact the server administrator."
127127
},
128+
"Error043": {
129+
"message": "E043: Failsafe: The current sync run would increase your bookmarks count by {0}%. Refusing to execute. Disable this failsafe in the profile settings if you want to proceed anyway."
130+
},
128131
"LabelWebdavurl": {
129132
"message": "WebDAV URL"
130133
},
@@ -511,16 +514,16 @@
511514
"message": "Failsafe"
512515
},
513516
"DescriptionFailsafe": {
514-
"message": "Sometimes configuration errors or software bugs can cause the unintended removal of data, which is then lost. To prevent that from happening, floccus will not delete more than 50% of your bookmarks in one go unless you disable this failsafe here."
517+
"message": "Sometimes configuration errors or software bugs can cause the unintended removal of data, which is then lost, or the unintended duplication of data, which is then hard to sort out. To prevent this from happening, floccus will not delete more than 20% of your bookmarks in one go and will also not add more than 20% to your bookmarks count in one go, unless you disable this failsafe here."
515518
},
516519
"LabelFailsafeon": {
517-
"message": "Enabled. Don't delete more than 50% of my local bookmarks without asking me first. (Recommended)"
520+
"message": "Enabled. Will not delete or add more than 20% from/to your local bookmarks without asking you first. (Recommended)"
518521
},
519522
"LabelFailsafeoff": {
520-
"message": "Disabled. Allow the removal of more than 50% of my local bookmarks without confirming with me."
523+
"message": "Disabled. Will allow removing or adding more than 20% from/to your local bookmarks without confirming with you."
521524
},
522525
"StatusFailsafeoff": {
523-
"message": "Failsafe disabled. You are at risk for unintended data loss. It is recommended to enable the failsafe in the profile settings."
526+
"message": "Failsafe disabled. You are at risk for unintended data loss or duplication. It is recommended to enable the failsafe in the profile settings."
524527
},
525528
"LabelAdaptergoogledrive": {
526529
"message": "Google Drive"

src/errors/Error.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,14 @@ export class InterruptedSyncError extends FloccusError {
240240

241241
// code 28 is unused
242242

243-
export class FailsafeError extends FloccusError {
243+
export class DeletionFailsafeError extends FloccusError {
244244
public percent: number
245245

246246
constructor(percent:number) {
247247
super(`E029: Failsafe: The current sync run would delete ${percent}% of your bookmarks. Refusing to execute. Disable this failsafe in the profile settings if you want to proceed anyway.`)
248248
this.code = 29
249249
this.percent = percent
250-
Object.setPrototypeOf(this, FailsafeError.prototype)
250+
Object.setPrototypeOf(this, DeletionFailsafeError.prototype)
251251
}
252252
}
253253

@@ -357,4 +357,15 @@ export class FileSizeUnknown extends FloccusError {
357357
this.code = 42
358358
Object.setPrototypeOf(this, FileSizeUnknown.prototype)
359359
}
360+
}
361+
362+
export class AdditionFailsafeError extends FloccusError {
363+
public percent: number
364+
365+
constructor(percent:number) {
366+
super(`E043: Failsafe: The current sync run would increase your bookmarks count by ${percent}%. Refusing to execute. Disable this failsafe in the profile settings if you want to proceed anyway.`)
367+
this.code = 43
368+
this.percent = percent
369+
Object.setPrototypeOf(this, AdditionFailsafeError.prototype)
370+
}
360371
}

src/lib/browser/BrowserAccount.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import browser from '../browser-api'
44
import AdapterFactory from '../AdapterFactory'
55
import Account from '../Account'
66
import {
7+
AdditionFailsafeError,
78
CreateBookmarkError,
8-
FailsafeError, FloccusError,
9+
DeletionFailsafeError, FloccusError,
910
HttpError,
1011
InconsistentBookmarksExistenceError, LockFileError,
1112
MissingItemOrderError,
@@ -106,7 +107,10 @@ export default class BrowserAccount extends Account {
106107
if (er instanceof LockFileError) {
107108
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.status, er.lockFile])
108109
}
109-
if (er instanceof FailsafeError) {
110+
if (er instanceof DeletionFailsafeError) {
111+
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.percent])
112+
}
113+
if (er instanceof AdditionFailsafeError) {
110114
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.percent])
111115
}
112116
if (er instanceof CreateBookmarkError) {

src/lib/native/NativeAccount.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import AdapterFactory from '../AdapterFactory'
44
import Account from '../Account'
55
import { IAccountData } from '../interfaces/AccountStorage'
66
import {
7+
AdditionFailsafeError,
78
CreateBookmarkError,
8-
FailsafeError, FloccusError,
9+
DeletionFailsafeError, FloccusError,
910
HttpError,
1011
InconsistentBookmarksExistenceError, LockFileError,
1112
MissingItemOrderError,
@@ -84,7 +85,10 @@ export default class NativeAccount extends Account {
8485
if (er instanceof LockFileError) {
8586
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.status, er.lockFile])
8687
}
87-
if (er instanceof FailsafeError) {
88+
if (er instanceof DeletionFailsafeError) {
89+
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.percent])
90+
}
91+
if (er instanceof AdditionFailsafeError) {
8892
return i18n.getMessage('Error' + String(er.code).padStart(3, '0'), [er.percent])
8993
}
9094
if (er instanceof CreateBookmarkError) {

src/lib/strategies/Default.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { throttle } from 'throttle-debounce'
2323
import Mappings, { MappingSnapshot } from '../Mappings'
2424
import TResource, { OrderFolderResource, TLocalTree } from '../interfaces/Resource'
2525
import { TAdapter } from '../interfaces/Adapter'
26-
import { CancelledSyncError, FailsafeError } from '../../errors/Error'
26+
import { AdditionFailsafeError, CancelledSyncError, DeletionFailsafeError } from '../../errors/Error'
2727

2828
import NextcloudBookmarksAdapter from '../adapters/NextcloudBookmarks'
2929
import CachingAdapter from '../adapters/Caching'
@@ -246,7 +246,10 @@ export default class SyncProcess {
246246

247247
Logger.log({localPlan: this.localPlanStage2, serverPlan: this.serverPlanStage2})
248248

249-
this.applyFailsafe(this.localPlanStage2.REMOVE)
249+
this.applyDeletionFailsafe(this.localTreeRoot, this.localPlanStage2.REMOVE)
250+
this.applyAdditionFailsafe(this.localTreeRoot, this.localPlanStage2.CREATE)
251+
this.applyDeletionFailsafe(this.serverTreeRoot, this.serverPlanStage2.REMOVE)
252+
this.applyAdditionFailsafe(this.serverTreeRoot, this.serverPlanStage2.CREATE)
250253

251254
if (!this.localDonePlan) {
252255
this.localDonePlan = {
@@ -360,16 +363,30 @@ export default class SyncProcess {
360363
this.serverTreeRoot.createIndex()
361364
}
362365

363-
protected applyFailsafe(removals: Diff<TItemLocation, TItemLocation, RemoveAction<TItemLocation, TItemLocation>>) {
364-
const localCountTotal = this.localTreeRoot.count()
365-
const localCountDeleted = removals.getActions().reduce((count, action) => count + action.payload.count(), 0)
366+
protected applyDeletionFailsafe(tree: Folder<TItemLocation>, removals: Diff<TItemLocation, TItemLocation, RemoveAction<TItemLocation, TItemLocation>>) {
367+
const countTotal = tree.count()
368+
const countDeleted = removals.getActions().reduce((count, action) => count + action.payload.count(), 0)
366369

367-
Logger.log('Checking failsafe: ' + localCountDeleted + '/' + localCountTotal + '=' + (localCountDeleted / localCountTotal))
370+
Logger.log('Checking deletion failsafe: ' + countDeleted + '/' + countTotal + '=' + (countDeleted / countTotal))
368371
// Failsafe kicks in if more than 20% is deleted or more than 1k bookmarks
369-
if ((localCountTotal > 5 && localCountDeleted / localCountTotal > 0.2) || localCountDeleted > 1000) {
372+
if ((countTotal > 5 && countDeleted / countTotal > 0.2) || countDeleted > 1000) {
370373
const failsafe = this.server.getData().failsafe
371374
if (failsafe !== false || typeof failsafe === 'undefined') {
372-
throw new FailsafeError(Math.ceil((localCountDeleted / localCountTotal) * 100))
375+
throw new DeletionFailsafeError(Math.ceil((countDeleted / countTotal) * 100))
376+
}
377+
}
378+
}
379+
380+
protected applyAdditionFailsafe(tree: Folder<TItemLocation>, creations: Diff<TItemLocation, TItemLocation, CreateAction<TItemLocation, TItemLocation>>) {
381+
const countTotal = tree.count()
382+
const countAdded = creations.getActions().reduce((count, action) => count + action.payload.count(), 0)
383+
384+
Logger.log('Checking addition failsafe: ' + countAdded + '/' + countTotal + '=' + (countAdded / countTotal))
385+
// Failsafe kicks in if more than 20% is added or more than 1k bookmarks
386+
if (countTotal > 5 && (countAdded / countTotal > 0.2 || countAdded > 1000)) {
387+
const failsafe = this.server.getData().failsafe
388+
if (failsafe !== false || typeof failsafe === 'undefined') {
389+
throw new AdditionFailsafeError(Math.ceil((countAdded / countTotal) * 100))
373390
}
374391
}
375392
}

src/lib/strategies/Unidirectional.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ export default class UnidirectionalSyncProcess extends DefaultStrategy {
158158
this.actionsPlanned = Object.values(this.revertPlan).reduce((acc, diff) => diff.getActions().length + acc, 0)
159159

160160
if (this.direction === ItemLocation.LOCAL) {
161-
this.applyFailsafe(this.revertPlan.REMOVE)
161+
this.applyDeletionFailsafe(this.localTreeRoot, this.revertPlan.REMOVE)
162+
this.applyAdditionFailsafe(this.localTreeRoot, this.revertPlan.CREATE)
163+
} else {
164+
this.applyDeletionFailsafe(this.serverTreeRoot, this.revertPlan.REMOVE)
165+
this.applyAdditionFailsafe(this.serverTreeRoot, this.revertPlan.CREATE)
162166
}
163167

164168
if (this.canceled) {

src/test/test.js

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import DefunctCrypto from '../lib/DefunctCrypto'
1111
import Controller from '../lib/Controller'
1212
import FakeAdapter from '../lib/adapters/Fake'
1313
import BrowserTree from '../lib/browser/BrowserTree'
14+
import { AdditionFailsafeError, DeletionFailsafeError } from '../errors/Error'
1415

1516
chai.use(chaiAsPromised)
1617
const expect = chai.expect
@@ -2174,7 +2175,7 @@ describe('Floccus', function() {
21742175
expect(localTree.findBookmark(bookmark1.id)).to.be.ok
21752176
expect(localTree.findBookmark(bookmark2.id)).to.be.ok
21762177
})
2177-
it('should error when deleting too much local data', async function() {
2178+
it('should error when deleting too much local data (failsafe)', async function() {
21782179
if (ACCOUNT_DATA.noCache) {
21792180
this.skip()
21802181
}
@@ -2188,6 +2189,7 @@ describe('Floccus', function() {
21882189
title: 'bar',
21892190
parentId: fooFolder.id
21902191
})
2192+
// We need more than 5 bookmarks to be above the negligibility threshold
21912193
await Promise.all([
21922194
'http://ur.l/',
21932195
'http://ur.ll/',
@@ -2222,6 +2224,176 @@ describe('Floccus', function() {
22222224

22232225
await account.sync()
22242226
expect(account.getData().error).to.be.ok // should have errored
2227+
expect(account.getData().error).to.include((new DeletionFailsafeError).code)
2228+
})
2229+
it('should error when adding too much local data (failsafe)', async function() {
2230+
if (ACCOUNT_DATA.noCache) {
2231+
this.skip()
2232+
}
2233+
2234+
const localRoot = account.getData().localRoot
2235+
const fooFolder = await browser.bookmarks.create({
2236+
title: 'foo',
2237+
parentId: localRoot
2238+
})
2239+
const barFolder = await browser.bookmarks.create({
2240+
title: 'bar',
2241+
parentId: fooFolder.id
2242+
})
2243+
// Create 6 bookmarks to be above the negligibility threshold
2244+
await Promise.all([
2245+
'http://ur.1l/',
2246+
'http://ur2.l/',
2247+
'http://ur3.l/',
2248+
'http://ur4.l/',
2249+
'http://ur5.l/',
2250+
'http://ur6.l/'
2251+
].map(url => browser.bookmarks.create({
2252+
title: 'url',
2253+
url,
2254+
parentId: barFolder.id
2255+
})))
2256+
await account.sync()
2257+
expect(account.getData().error).to.not.be.ok
2258+
2259+
// Simulate an extreme increase of bookmarks
2260+
const tree = await getAllBookmarks(account)
2261+
await withSyncConnection(account, async() => {
2262+
await Promise.all([
2263+
'http://ur.1l/',
2264+
'http://ur2.l/',
2265+
'http://ur3.l/',
2266+
'http://ur4.l/',
2267+
'http://ur5.l/',
2268+
'http://ur6.l/',
2269+
'http://ur7.l/',
2270+
'http://ur8.l/',
2271+
'http://ur9.l/',
2272+
'http://ur10.l/',
2273+
'http://ur.11l/',
2274+
'http://ur12.l/',
2275+
'http://ur13.l/',
2276+
'http://ur14.l/',
2277+
'http://ur15.l/',
2278+
'http://ur16.l/',
2279+
'http://ur17.l/',
2280+
'http://ur18.l/',
2281+
'http://ur19.l/',
2282+
'http://ur20.l/',
2283+
].map(url => account.server.createBookmark(new Bookmark({
2284+
title: 'url',
2285+
url,
2286+
parentId: tree.id
2287+
}))))
2288+
})
2289+
2290+
await account.sync()
2291+
expect(account.getData().error).to.be.ok // should have errored
2292+
expect(account.getData().error).to.include((new AdditionFailsafeError).code)
2293+
})
2294+
it('should error when deleting too much remote data (failsafe)', async function() {
2295+
if (ACCOUNT_DATA.noCache) {
2296+
this.skip()
2297+
}
2298+
2299+
const localRoot = account.getData().localRoot
2300+
const fooFolder = await browser.bookmarks.create({
2301+
title: 'foo',
2302+
parentId: localRoot
2303+
})
2304+
const barFolder = await browser.bookmarks.create({
2305+
title: 'bar',
2306+
parentId: fooFolder.id
2307+
})
2308+
// We need more than 5 bookmarks to be above the negligibility threshold
2309+
await Promise.all([
2310+
'http://ur.l/',
2311+
'http://ur.ll/',
2312+
'http://ur2.l/',
2313+
'http://ur3.l/',
2314+
'http://ur4.l/',
2315+
'http://ur5.l/',
2316+
'http://ur6.l/',
2317+
'http://ur7.l/',
2318+
'http://ur8.l/',
2319+
'http://ur9.l/',
2320+
'http://ur10.l/',
2321+
].map(url => browser.bookmarks.create({
2322+
title: 'url',
2323+
url,
2324+
parentId: barFolder.id
2325+
})))
2326+
await account.sync()
2327+
expect(account.getData().error).to.not.be.ok
2328+
2329+
// Remove everything
2330+
await browser.bookmarks.removeTree(barFolder.id)
2331+
2332+
await account.sync()
2333+
expect(account.getData().error).to.be.ok // should have errored
2334+
expect(account.getData().error).to.include((new DeletionFailsafeError).code)
2335+
})
2336+
it('should error when adding too much remote data (failsafe)', async function() {
2337+
if (ACCOUNT_DATA.noCache) {
2338+
this.skip()
2339+
}
2340+
2341+
const localRoot = account.getData().localRoot
2342+
const fooFolder = await browser.bookmarks.create({
2343+
title: 'foo',
2344+
parentId: localRoot
2345+
})
2346+
const barFolder = await browser.bookmarks.create({
2347+
title: 'bar',
2348+
parentId: fooFolder.id
2349+
})
2350+
// Create 6 bookmarks to be above the negligibility threshold
2351+
await Promise.all([
2352+
'http://ur.1l/',
2353+
'http://ur2.l/',
2354+
'http://ur3.l/',
2355+
'http://ur4.l/',
2356+
'http://ur5.l/',
2357+
'http://ur6.l/'
2358+
].map(url => browser.bookmarks.create({
2359+
title: 'url',
2360+
url,
2361+
parentId: barFolder.id
2362+
})))
2363+
await account.sync()
2364+
expect(account.getData().error).to.not.be.ok
2365+
2366+
// Simulate an extreme increase of bookmarks
2367+
await Promise.all([
2368+
'http://ur.1l/',
2369+
'http://ur2.l/',
2370+
'http://ur3.l/',
2371+
'http://ur4.l/',
2372+
'http://ur5.l/',
2373+
'http://ur6.l/',
2374+
'http://ur7.l/',
2375+
'http://ur8.l/',
2376+
'http://ur9.l/',
2377+
'http://ur10.l/',
2378+
'http://ur.11l/',
2379+
'http://ur12.l/',
2380+
'http://ur13.l/',
2381+
'http://ur14.l/',
2382+
'http://ur15.l/',
2383+
'http://ur16.l/',
2384+
'http://ur17.l/',
2385+
'http://ur18.l/',
2386+
'http://ur19.l/',
2387+
'http://ur20.l/',
2388+
].map(url => browser.bookmarks.create({
2389+
title: 'url',
2390+
url,
2391+
parentId: barFolder.id
2392+
})))
2393+
2394+
await account.sync()
2395+
expect(account.getData().error).to.be.ok // should have errored
2396+
expect(account.getData().error).to.include((new AdditionFailsafeError).code)
22252397
})
22262398
it('should leave alone unaccepted bookmarks entirely', async function() {
22272399
if (!~ACCOUNT_DATA.type.indexOf('nextcloud')) {

0 commit comments

Comments
 (0)